support for tipping protocol changes
This commit is contained in:
parent
defbf625bd
commit
5ec344290e
175
src/dbTypes.ts
175
src/dbTypes.ts
@ -14,7 +14,6 @@
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Types for records stored in the wallet's database.
|
||||
*
|
||||
@ -36,11 +35,7 @@ import {
|
||||
TipResponse,
|
||||
} from "./talerTypes";
|
||||
|
||||
import {
|
||||
Index,
|
||||
Store,
|
||||
} from "./query";
|
||||
|
||||
import { Index, Store } from "./query";
|
||||
|
||||
/**
|
||||
* Current database version, should be incremented
|
||||
@ -50,7 +45,6 @@ import {
|
||||
*/
|
||||
export const WALLET_DB_VERSION = 26;
|
||||
|
||||
|
||||
/**
|
||||
* A reserve record as stored in the wallet's database.
|
||||
*/
|
||||
@ -81,12 +75,11 @@ export interface ReserveRecord {
|
||||
*/
|
||||
timestamp_depleted: number;
|
||||
|
||||
|
||||
/**
|
||||
* Time when the information about this reserve was posted to the bank.
|
||||
*
|
||||
*
|
||||
* Only applies if bankWithdrawStatusUrl is defined.
|
||||
*
|
||||
*
|
||||
* Set to 0 if that hasn't happened yet.
|
||||
*/
|
||||
timestamp_reserve_info_posted: number;
|
||||
@ -137,7 +130,6 @@ export interface ReserveRecord {
|
||||
bankWithdrawStatusUrl?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Auditor record as stored with currencies in the exchange database.
|
||||
*/
|
||||
@ -156,7 +148,6 @@ export interface AuditorRecord {
|
||||
expirationStamp: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exchange for currencies as stored in the wallet's currency
|
||||
* information database.
|
||||
@ -172,7 +163,6 @@ export interface ExchangeForCurrencyRecord {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Information about a currency as displayed in the wallet's database.
|
||||
*/
|
||||
@ -195,7 +185,6 @@ export interface CurrencyRecord {
|
||||
exchanges: ExchangeForCurrencyRecord[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Status of a denomination.
|
||||
*/
|
||||
@ -214,7 +203,6 @@ export enum DenominationStatus {
|
||||
VerifiedBad,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Denomination record as stored in the wallet's database.
|
||||
*/
|
||||
@ -321,7 +309,6 @@ export class DenominationRecord {
|
||||
static checked: (obj: any) => Denomination;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exchange record as stored in the wallet's database.
|
||||
*/
|
||||
@ -362,7 +349,6 @@ export interface ExchangeRecord {
|
||||
protocolVersion?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A coin that isn't yet signed by an exchange.
|
||||
*/
|
||||
@ -385,7 +371,6 @@ export interface PreCoinRecord {
|
||||
isFromTip: boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Planchet for a coin during refrehs.
|
||||
*/
|
||||
@ -408,7 +393,6 @@ export interface RefreshPreCoinRecord {
|
||||
blindingKey: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Status of a coin.
|
||||
*/
|
||||
@ -441,13 +425,8 @@ export enum CoinStatus {
|
||||
* Coin was dirty but can't be refreshed.
|
||||
*/
|
||||
Useless,
|
||||
/**
|
||||
* The coin was withdrawn for a tip that the user hasn't accepted yet.
|
||||
*/
|
||||
TainedByTip,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* CoinRecord as stored in the "coins" data store
|
||||
* of the wallet database.
|
||||
@ -506,7 +485,7 @@ export interface CoinRecord {
|
||||
* Reserve public key for the reserve we got this coin from,
|
||||
* or zero when we got the coin from refresh.
|
||||
*/
|
||||
reservePub: string|undefined;
|
||||
reservePub: string | undefined;
|
||||
|
||||
/**
|
||||
* Status of the coin.
|
||||
@ -514,7 +493,6 @@ export interface CoinRecord {
|
||||
status: CoinStatus;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Proposal record, stored in the wallet's database.
|
||||
*/
|
||||
@ -576,7 +554,6 @@ export class ProposalDownloadRecord {
|
||||
static checked: (obj: any) => ProposalDownloadRecord;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wire fees for an exchange.
|
||||
*/
|
||||
@ -592,7 +569,6 @@ export interface ExchangeWireFeesRecord {
|
||||
feesForType: { [wireMethod: string]: WireFee[] };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Status of a tip we got from a merchant.
|
||||
*/
|
||||
@ -613,12 +589,7 @@ export interface TipRecord {
|
||||
*/
|
||||
amount: AmountJson;
|
||||
|
||||
/**
|
||||
* Coin public keys from the planchets.
|
||||
* This field is redundant and used for indexing the record via
|
||||
* a multi-entry index to look up tip records by coin public key.
|
||||
*/
|
||||
coinPubs: string[];
|
||||
totalFees: AmountJson;
|
||||
|
||||
/**
|
||||
* Timestamp, the tip can't be picked up anymore after this deadline.
|
||||
@ -641,7 +612,14 @@ export interface TipRecord {
|
||||
* Planchets, the members included in TipPlanchetDetail will be sent to the
|
||||
* merchant.
|
||||
*/
|
||||
planchets: TipPlanchet[];
|
||||
planchets?: TipPlanchet[];
|
||||
|
||||
/**
|
||||
* Coin public keys from the planchets.
|
||||
* This field is redundant and used for indexing the record via
|
||||
* a multi-entry index to look up tip records by coin public key.
|
||||
*/
|
||||
coinPubs: string[];
|
||||
|
||||
/**
|
||||
* Response if the merchant responded,
|
||||
@ -657,11 +635,12 @@ export interface TipRecord {
|
||||
/**
|
||||
* URL to go to once the tip has been accepted.
|
||||
*/
|
||||
nextUrl: string;
|
||||
nextUrl?: string;
|
||||
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
pickupUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ongoing refresh
|
||||
@ -740,7 +719,6 @@ export interface RefreshSessionRecord {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tipping planchet stored in the database.
|
||||
*/
|
||||
@ -754,7 +732,6 @@ export interface TipPlanchet {
|
||||
denomPub: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wire fee for one wire method as stored in the
|
||||
* wallet's database.
|
||||
@ -861,7 +838,6 @@ export interface PurchaseRecord {
|
||||
abortDone: boolean;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Information about wire information for bank accounts we withdrew coins from.
|
||||
*/
|
||||
@ -869,7 +845,6 @@ export interface SenderWireRecord {
|
||||
paytoUri: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configuration key/value entries to configure
|
||||
* the wallet.
|
||||
@ -879,7 +854,6 @@ export interface ConfigRecord {
|
||||
value: any;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Coin that we're depositing ourselves.
|
||||
*/
|
||||
@ -893,7 +867,6 @@ export interface DepositCoin {
|
||||
depositedSig?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Record stored in the wallet's database when the user sends coins back to
|
||||
* their own bank account. Stores the status of coins that are deposited to
|
||||
@ -927,7 +900,6 @@ export interface CoinsReturnRecord {
|
||||
wire: any;
|
||||
}
|
||||
|
||||
|
||||
/* tslint:disable:completed-docs */
|
||||
|
||||
/**
|
||||
@ -939,7 +911,11 @@ export namespace Stores {
|
||||
super("exchanges", { keyPath: "baseUrl" });
|
||||
}
|
||||
|
||||
pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", "masterPublicKey");
|
||||
pubKeyIndex = new Index<string, ExchangeRecord>(
|
||||
this,
|
||||
"pubKeyIndex",
|
||||
"masterPublicKey",
|
||||
);
|
||||
}
|
||||
|
||||
class CoinsStore extends Store<CoinRecord> {
|
||||
@ -947,8 +923,16 @@ export namespace Stores {
|
||||
super("coins", { keyPath: "coinPub" });
|
||||
}
|
||||
|
||||
exchangeBaseUrlIndex = new Index<string, CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl");
|
||||
denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", "denomPub");
|
||||
exchangeBaseUrlIndex = new Index<string, CoinRecord>(
|
||||
this,
|
||||
"exchangeBaseUrl",
|
||||
"exchangeBaseUrl",
|
||||
);
|
||||
denomPubIndex = new Index<string, CoinRecord>(
|
||||
this,
|
||||
"denomPubIndex",
|
||||
"denomPub",
|
||||
);
|
||||
}
|
||||
|
||||
class ProposalsStore extends Store<ProposalDownloadRecord> {
|
||||
@ -958,8 +942,16 @@ export namespace Stores {
|
||||
keyPath: "id",
|
||||
});
|
||||
}
|
||||
urlIndex = new Index<string, ProposalDownloadRecord>(this, "urlIndex", "url");
|
||||
timestampIndex = new Index<string, ProposalDownloadRecord>(this, "timestampIndex", "timestamp");
|
||||
urlIndex = new Index<string, ProposalDownloadRecord>(
|
||||
this,
|
||||
"urlIndex",
|
||||
"url",
|
||||
);
|
||||
timestampIndex = new Index<string, ProposalDownloadRecord>(
|
||||
this,
|
||||
"timestampIndex",
|
||||
"timestamp",
|
||||
);
|
||||
}
|
||||
|
||||
class PurchasesStore extends Store<PurchaseRecord> {
|
||||
@ -967,23 +959,46 @@ export namespace Stores {
|
||||
super("purchases", { keyPath: "contractTermsHash" });
|
||||
}
|
||||
|
||||
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this,
|
||||
"fulfillmentUrlIndex",
|
||||
"contractTerms.fulfillment_url");
|
||||
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", "contractTerms.order_id");
|
||||
timestampIndex = new Index<string, PurchaseRecord>(this, "timestampIndex", "timestamp");
|
||||
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
|
||||
this,
|
||||
"fulfillmentUrlIndex",
|
||||
"contractTerms.fulfillment_url",
|
||||
);
|
||||
orderIdIndex = new Index<string, PurchaseRecord>(
|
||||
this,
|
||||
"orderIdIndex",
|
||||
"contractTerms.order_id",
|
||||
);
|
||||
timestampIndex = new Index<string, PurchaseRecord>(
|
||||
this,
|
||||
"timestampIndex",
|
||||
"timestamp",
|
||||
);
|
||||
}
|
||||
|
||||
class DenominationsStore extends Store<DenominationRecord> {
|
||||
constructor() {
|
||||
// cast needed because of bug in type annotations
|
||||
super("denominations",
|
||||
{keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath});
|
||||
super("denominations", {
|
||||
keyPath: (["exchangeBaseUrl", "denomPub"] as any) as IDBKeyPath,
|
||||
});
|
||||
}
|
||||
|
||||
denomPubHashIndex = new Index<string, DenominationRecord>(this, "denomPubHashIndex", "denomPubHash");
|
||||
exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrlIndex", "exchangeBaseUrl");
|
||||
denomPubIndex = new Index<string, DenominationRecord>(this, "denomPubIndex", "denomPub");
|
||||
denomPubHashIndex = new Index<string, DenominationRecord>(
|
||||
this,
|
||||
"denomPubHashIndex",
|
||||
"denomPubHash",
|
||||
);
|
||||
exchangeBaseUrlIndex = new Index<string, DenominationRecord>(
|
||||
this,
|
||||
"exchangeBaseUrlIndex",
|
||||
"exchangeBaseUrl",
|
||||
);
|
||||
denomPubIndex = new Index<string, DenominationRecord>(
|
||||
this,
|
||||
"denomPubIndex",
|
||||
"denomPub",
|
||||
);
|
||||
}
|
||||
|
||||
class CurrenciesStore extends Store<CurrencyRecord> {
|
||||
@ -1008,16 +1023,35 @@ export namespace Stores {
|
||||
constructor() {
|
||||
super("reserves", { keyPath: "reserve_pub" });
|
||||
}
|
||||
timestampCreatedIndex = new Index<string, ReserveRecord>(this, "timestampCreatedIndex", "created");
|
||||
timestampConfirmedIndex = new Index<string, ReserveRecord>(this, "timestampConfirmedIndex", "timestamp_confirmed");
|
||||
timestampDepletedIndex = new Index<string, ReserveRecord>(this, "timestampDepletedIndex", "timestamp_depleted");
|
||||
timestampCreatedIndex = new Index<string, ReserveRecord>(
|
||||
this,
|
||||
"timestampCreatedIndex",
|
||||
"created",
|
||||
);
|
||||
timestampConfirmedIndex = new Index<string, ReserveRecord>(
|
||||
this,
|
||||
"timestampConfirmedIndex",
|
||||
"timestamp_confirmed",
|
||||
);
|
||||
timestampDepletedIndex = new Index<string, ReserveRecord>(
|
||||
this,
|
||||
"timestampDepletedIndex",
|
||||
"timestamp_depleted",
|
||||
);
|
||||
}
|
||||
|
||||
class TipsStore extends Store<TipRecord> {
|
||||
constructor() {
|
||||
super("tips", { keyPath: ["tipId", "merchantDomain"] as any as IDBKeyPath });
|
||||
super("tips", {
|
||||
keyPath: (["tipId", "merchantDomain"] as any) as IDBKeyPath,
|
||||
});
|
||||
}
|
||||
coinPubIndex = new Index<string, TipRecord>(this, "coinPubIndex", "coinPubs", { multiEntry: true });
|
||||
coinPubIndex = new Index<string, TipRecord>(
|
||||
this,
|
||||
"coinPubIndex",
|
||||
"coinPubs",
|
||||
{ multiEntry: true },
|
||||
);
|
||||
}
|
||||
|
||||
class SenderWiresStore extends Store<SenderWireRecord> {
|
||||
@ -1027,15 +1061,22 @@ export namespace Stores {
|
||||
}
|
||||
|
||||
export const coins = new CoinsStore();
|
||||
export const coinsReturns = new Store<CoinsReturnRecord>("coinsReturns", {keyPath: "contractTermsHash"});
|
||||
export const coinsReturns = new Store<CoinsReturnRecord>("coinsReturns", {
|
||||
keyPath: "contractTermsHash",
|
||||
});
|
||||
export const config = new ConfigStore();
|
||||
export const currencies = new CurrenciesStore();
|
||||
export const denominations = new DenominationsStore();
|
||||
export const exchangeWireFees = new ExchangeWireFeesStore();
|
||||
export const exchanges = new ExchangeStore();
|
||||
export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
|
||||
export const precoins = new Store<PreCoinRecord>("precoins", {
|
||||
keyPath: "coinPub",
|
||||
});
|
||||
export const proposals = new ProposalsStore();
|
||||
export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "id", autoIncrement: true});
|
||||
export const refresh = new Store<RefreshSessionRecord>("refresh", {
|
||||
keyPath: "id",
|
||||
autoIncrement: true,
|
||||
});
|
||||
export const reserves = new ReservesStore();
|
||||
export const purchases = new PurchasesStore();
|
||||
export const tips = new TipsStore();
|
||||
|
@ -127,7 +127,7 @@ program
|
||||
});
|
||||
|
||||
program
|
||||
.command("withdraw-url <withdraw-url>")
|
||||
.command("withdraw-uri <withdraw-uri>")
|
||||
.action(async (withdrawUrl, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("withdrawing", withdrawUrl);
|
||||
@ -166,7 +166,21 @@ program
|
||||
});
|
||||
|
||||
program
|
||||
.command("pay-url <pay-url>")
|
||||
.command("tip-uri <tip-uri>")
|
||||
.action(async (tipUri, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
console.log("getting tip", tipUri);
|
||||
const wallet = await getDefaultNodeWallet({
|
||||
persistentStoragePath: walletDbPath,
|
||||
});
|
||||
const res = await wallet.getTipStatus(tipUri);
|
||||
console.log("tip status", res);
|
||||
await wallet.acceptTip(tipUri);
|
||||
wallet.stop();
|
||||
});
|
||||
|
||||
program
|
||||
.command("pay-uri <pay-uri")
|
||||
.option("-y, --yes", "automatically answer yes to prompts")
|
||||
.action(async (payUrl, cmdObj) => {
|
||||
applyVerbose(program.verbose);
|
||||
|
@ -32,7 +32,6 @@ import * as Amounts from "./amounts";
|
||||
|
||||
import { timestampCheck } from "./helpers";
|
||||
|
||||
|
||||
/**
|
||||
* Denomination as found in the /keys response from the exchange.
|
||||
*/
|
||||
@ -114,7 +113,6 @@ export class Denomination {
|
||||
static checked: (obj: any) => Denomination;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Signature by the auditor that a particular denomination key is audited.
|
||||
*/
|
||||
@ -133,7 +131,6 @@ export class AuditorDenomSig {
|
||||
auditor_sig: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Auditor information as given by the exchange in /keys.
|
||||
*/
|
||||
@ -158,7 +155,6 @@ export class Auditor {
|
||||
denomination_keys: AuditorDenomSig[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request that we send to the exchange to get a payback.
|
||||
*/
|
||||
@ -191,7 +187,6 @@ export interface PaybackRequest {
|
||||
coin_sig: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Response that we get from the exchange for a payback request.
|
||||
*/
|
||||
@ -242,7 +237,6 @@ export class PaybackConfirmation {
|
||||
static checked: (obj: any) => PaybackConfirmation;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deposit permission for a single coin.
|
||||
*/
|
||||
@ -274,7 +268,6 @@ export interface CoinPaySig {
|
||||
exchange_url: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Information about an exchange as stored inside a
|
||||
* merchant's contract terms.
|
||||
@ -300,11 +293,10 @@ export class ExchangeHandle {
|
||||
static checked: (obj: any) => ExchangeHandle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Contract terms from a merchant.
|
||||
*/
|
||||
@Checkable.Class({validate: true})
|
||||
@Checkable.Class({ validate: true })
|
||||
export class ContractTerms {
|
||||
static validate(x: ContractTerms) {
|
||||
if (x.exchanges.length === 0) {
|
||||
@ -447,7 +439,6 @@ export class ContractTerms {
|
||||
static checked: (obj: any) => ContractTerms;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Payment body sent to the merchant's /pay.
|
||||
*/
|
||||
@ -474,7 +465,6 @@ export interface PayReq {
|
||||
mode: "pay" | "abort-refund";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refund permission in the format that the merchant gives it to us.
|
||||
*/
|
||||
@ -516,7 +506,6 @@ export class MerchantRefundPermission {
|
||||
static checked: (obj: any) => MerchantRefundPermission;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refund request sent to the exchange.
|
||||
*/
|
||||
@ -560,7 +549,6 @@ export interface RefundRequest {
|
||||
merchant_sig: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Response for a refund pickup or a /pay in abort mode.
|
||||
*/
|
||||
@ -591,7 +579,6 @@ export class MerchantRefundResponse {
|
||||
static checked: (obj: any) => MerchantRefundResponse;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Planchet detail sent to the merchant.
|
||||
*/
|
||||
@ -607,7 +594,6 @@ export interface TipPlanchetDetail {
|
||||
coin_ev: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request sent to the merchant to pick up a tip.
|
||||
*/
|
||||
@ -641,7 +627,6 @@ export class ReserveSigSingleton {
|
||||
static checked: (obj: any) => ReserveSigSingleton;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Response to /reserve/status
|
||||
*/
|
||||
@ -689,56 +674,6 @@ export class TipResponse {
|
||||
static checked: (obj: any) => TipResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token containing all the information for the wallet
|
||||
* to process a tip. Given by the merchant to the wallet.
|
||||
*/
|
||||
@Checkable.Class()
|
||||
export class TipToken {
|
||||
/**
|
||||
* Expiration for the tip.
|
||||
*/
|
||||
@Checkable.String(timestampCheck)
|
||||
expiration: string;
|
||||
|
||||
/**
|
||||
* URL of the exchange that the tip can be withdrawn from.
|
||||
*/
|
||||
@Checkable.String()
|
||||
exchange_url: string;
|
||||
|
||||
/**
|
||||
* Merchant's URL to pick up the tip.
|
||||
*/
|
||||
@Checkable.String()
|
||||
pickup_url: string;
|
||||
|
||||
/**
|
||||
* Merchant-chosen tip identifier.
|
||||
*/
|
||||
@Checkable.String()
|
||||
tip_id: string;
|
||||
|
||||
/**
|
||||
* Amount of tip.
|
||||
*/
|
||||
@Checkable.String()
|
||||
amount: string;
|
||||
|
||||
/**
|
||||
* URL to navigate after finishing tip processing.
|
||||
*/
|
||||
@Checkable.String()
|
||||
next_url: string;
|
||||
|
||||
/**
|
||||
* Create a TipToken from untyped JSON.
|
||||
* Validates the schema and throws on error.
|
||||
*/
|
||||
static checked: (obj: any) => TipToken;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Element of the payback list that the
|
||||
* exchange gives us in /keys.
|
||||
@ -752,11 +687,10 @@ export class Payback {
|
||||
h_denom_pub: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Structure that the exchange gives us in /keys.
|
||||
*/
|
||||
@Checkable.Class({extra: true})
|
||||
@Checkable.Class({ extra: true })
|
||||
export class KeysJson {
|
||||
/**
|
||||
* List of offered denominations.
|
||||
@ -808,7 +742,6 @@ export class KeysJson {
|
||||
static checked: (obj: any) => KeysJson;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wire fees as anounced by the exchange.
|
||||
*/
|
||||
@ -851,8 +784,7 @@ export class WireFeesJson {
|
||||
static checked: (obj: any) => WireFeesJson;
|
||||
}
|
||||
|
||||
|
||||
@Checkable.Class({extra: true})
|
||||
@Checkable.Class({ extra: true })
|
||||
export class AccountInfo {
|
||||
@Checkable.String()
|
||||
url: string;
|
||||
@ -861,10 +793,12 @@ export class AccountInfo {
|
||||
master_sig: string;
|
||||
}
|
||||
|
||||
|
||||
@Checkable.Class({extra: true})
|
||||
@Checkable.Class({ extra: true })
|
||||
export class ExchangeWireJson {
|
||||
@Checkable.Map(Checkable.String(), Checkable.List(Checkable.Value(() => WireFeesJson)))
|
||||
@Checkable.Map(
|
||||
Checkable.String(),
|
||||
Checkable.List(Checkable.Value(() => WireFeesJson)),
|
||||
)
|
||||
fees: { [methodName: string]: WireFeesJson[] };
|
||||
|
||||
@Checkable.List(Checkable.Value(() => AccountInfo))
|
||||
@ -873,18 +807,16 @@ export class ExchangeWireJson {
|
||||
static checked: (obj: any) => ExchangeWireJson;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wire detail, arbitrary object that must at least
|
||||
* contain a "type" key.
|
||||
*/
|
||||
export type WireDetail = object & { type: string };
|
||||
|
||||
|
||||
/**
|
||||
* Proposal returned from the contract URL.
|
||||
*/
|
||||
@Checkable.Class({extra: true})
|
||||
@Checkable.Class({ extra: true })
|
||||
export class Proposal {
|
||||
/**
|
||||
* Contract terms for the propoal.
|
||||
@ -909,7 +841,7 @@ export class Proposal {
|
||||
/**
|
||||
* Response from the internal merchant API.
|
||||
*/
|
||||
@Checkable.Class({extra: true})
|
||||
@Checkable.Class({ extra: true })
|
||||
export class CheckPaymentResponse {
|
||||
@Checkable.Boolean()
|
||||
paid: boolean;
|
||||
@ -939,7 +871,7 @@ export class CheckPaymentResponse {
|
||||
/**
|
||||
* Response from the bank.
|
||||
*/
|
||||
@Checkable.Class({extra: true})
|
||||
@Checkable.Class({ extra: true })
|
||||
export class WithdrawOperationStatusResponse {
|
||||
@Checkable.Boolean()
|
||||
selection_done: boolean;
|
||||
@ -967,4 +899,34 @@ export class WithdrawOperationStatusResponse {
|
||||
* member.
|
||||
*/
|
||||
static checked: (obj: any) => WithdrawOperationStatusResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from the merchant.
|
||||
*/
|
||||
@Checkable.Class({ extra: true })
|
||||
export class TipPickupGetResponse {
|
||||
@Checkable.AnyObject()
|
||||
extra: any;
|
||||
|
||||
@Checkable.String()
|
||||
amount: string;
|
||||
|
||||
@Checkable.String()
|
||||
amount_left: string;
|
||||
|
||||
@Checkable.String()
|
||||
exchange_url: string;
|
||||
|
||||
@Checkable.String()
|
||||
stamp_expire: string;
|
||||
|
||||
@Checkable.String()
|
||||
stamp_created: string;
|
||||
|
||||
/**
|
||||
* Verify that a value matches the schema of this class and convert it into a
|
||||
* member.
|
||||
*/
|
||||
static checked: (obj: any) => TipPickupGetResponse;
|
||||
}
|
||||
|
@ -26,6 +26,13 @@ export interface WithdrawUriResult {
|
||||
statusUrl: string;
|
||||
}
|
||||
|
||||
export interface TipUriResult {
|
||||
tipPickupUrl: string;
|
||||
tipId: string;
|
||||
merchantInstance: string;
|
||||
merchantOrigin: string;
|
||||
}
|
||||
|
||||
export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
|
||||
const parsedUri = new URI(s);
|
||||
if (parsedUri.scheme() !== "taler") {
|
||||
@ -104,3 +111,47 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
||||
sessionId: maybeSessionid,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseTipUri(s: string): TipUriResult | undefined {
|
||||
const parsedUri = new URI(s);
|
||||
if (parsedUri.scheme() != "taler") {
|
||||
return undefined;
|
||||
}
|
||||
if (parsedUri.authority() != "tip") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let [_, host, maybePath, maybeInstance, tipId] = parsedUri.path().split("/");
|
||||
|
||||
if (!host) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!maybePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!tipId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (maybePath === "-") {
|
||||
maybePath = "public/tip-pickup";
|
||||
} else {
|
||||
maybePath = decodeURIComponent(maybePath);
|
||||
}
|
||||
if (maybeInstance === "-") {
|
||||
maybeInstance = "default";
|
||||
}
|
||||
|
||||
const tipPickupUrl = new URI(
|
||||
"https://" + host + "/" + decodeURIComponent(maybePath),
|
||||
).href();
|
||||
|
||||
return {
|
||||
tipPickupUrl,
|
||||
tipId: tipId,
|
||||
merchantInstance: maybeInstance,
|
||||
merchantOrigin: new URI(tipPickupUrl).origin(),
|
||||
};
|
||||
}
|
||||
|
270
src/wallet.ts
270
src/wallet.ts
@ -80,8 +80,8 @@ import {
|
||||
ReserveStatus,
|
||||
TipPlanchetDetail,
|
||||
TipResponse,
|
||||
TipToken,
|
||||
WithdrawOperationStatusResponse,
|
||||
TipPickupGetResponse,
|
||||
} from "./talerTypes";
|
||||
import {
|
||||
Badge,
|
||||
@ -109,7 +109,7 @@ import {
|
||||
AcceptWithdrawalResponse,
|
||||
} from "./walletTypes";
|
||||
import { openPromise } from "./promiseUtils";
|
||||
import { parsePayUri, parseWithdrawUri } from "./taleruri";
|
||||
import { parsePayUri, parseWithdrawUri, parseTipUri } from "./taleruri";
|
||||
|
||||
interface SpeculativePayData {
|
||||
payCoinInfo: PayCoinInfo;
|
||||
@ -345,7 +345,7 @@ export class Wallet {
|
||||
private timerGroup: TimerGroup;
|
||||
private speculativePayData: SpeculativePayData | undefined;
|
||||
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
|
||||
private activeTipOperations: { [s: string]: Promise<TipRecord> } = {};
|
||||
private activeTipOperations: { [s: string]: Promise<void> } = {};
|
||||
private activeProcessReserveOperations: {
|
||||
[reservePub: string]: Promise<void>;
|
||||
} = {};
|
||||
@ -1351,33 +1351,7 @@ export class Wallet {
|
||||
.add(Stores.coins, coin)
|
||||
.finish();
|
||||
|
||||
if (coin.status === CoinStatus.TainedByTip) {
|
||||
const tip = await this.q().getIndexed(
|
||||
Stores.tips.coinPubIndex,
|
||||
coin.coinPub,
|
||||
);
|
||||
if (!tip) {
|
||||
throw Error(
|
||||
`inconsistent DB: tip for coin pub ${coin.coinPub} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (tip.accepted) {
|
||||
console.log("untainting already accepted tip");
|
||||
// Transactionally set coin to fresh.
|
||||
const mutateCoin = (c: CoinRecord) => {
|
||||
if (c.status === CoinStatus.TainedByTip) {
|
||||
c.status = CoinStatus.Fresh;
|
||||
}
|
||||
return c;
|
||||
};
|
||||
await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin);
|
||||
// Show notifications only for accepted tips
|
||||
this.badge.showNotification();
|
||||
}
|
||||
} else {
|
||||
this.badge.showNotification();
|
||||
}
|
||||
this.badge.showNotification();
|
||||
|
||||
this.notifier.notify();
|
||||
op.resolve();
|
||||
@ -1566,7 +1540,7 @@ export class Wallet {
|
||||
denomSig,
|
||||
exchangeBaseUrl: pc.exchangeBaseUrl,
|
||||
reservePub: pc.reservePub,
|
||||
status: pc.isFromTip ? CoinStatus.TainedByTip : CoinStatus.Fresh,
|
||||
status: CoinStatus.Fresh,
|
||||
};
|
||||
return coin;
|
||||
}
|
||||
@ -1856,14 +1830,14 @@ export class Wallet {
|
||||
return { isTrusted, isAudited };
|
||||
}
|
||||
|
||||
async getWithdrawDetails(
|
||||
talerPayUri: string,
|
||||
async getWithdrawDetailsForUri(
|
||||
talerWithdrawUri: string,
|
||||
maybeSelectedExchange?: string,
|
||||
): Promise<WithdrawDetails> {
|
||||
const info = await this.downloadWithdrawInfo(talerPayUri);
|
||||
const info = await this.downloadWithdrawInfo(talerWithdrawUri);
|
||||
let rci: ReserveCreationInfo | undefined = undefined;
|
||||
if (maybeSelectedExchange) {
|
||||
rci = await this.getReserveCreationInfo(
|
||||
rci = await this.getWithdrawDetailsForAmount(
|
||||
maybeSelectedExchange,
|
||||
info.amount,
|
||||
);
|
||||
@ -1874,7 +1848,7 @@ export class Wallet {
|
||||
};
|
||||
}
|
||||
|
||||
async getReserveCreationInfo(
|
||||
async getWithdrawDetailsForAmount(
|
||||
baseUrl: string,
|
||||
amount: AmountJson,
|
||||
): Promise<ReserveCreationInfo> {
|
||||
@ -3331,14 +3305,13 @@ export class Wallet {
|
||||
return feeAcc;
|
||||
}
|
||||
|
||||
async processTip(tipToken: TipToken): Promise<TipRecord> {
|
||||
const merchantDomain = new URI(tipToken.pickup_url).origin();
|
||||
const key = tipToken.tip_id + merchantDomain;
|
||||
|
||||
async acceptTip(talerTipUri: string): Promise<void> {
|
||||
const { tipId, merchantOrigin } = await this.getTipStatus(talerTipUri);
|
||||
const key = `${tipId}${merchantOrigin}`;
|
||||
if (this.activeTipOperations[key]) {
|
||||
return this.activeTipOperations[key];
|
||||
}
|
||||
const p = this.processTipImpl(tipToken);
|
||||
const p = this.acceptTipImpl(tipId, merchantOrigin);
|
||||
this.activeTipOperations[key] = p;
|
||||
try {
|
||||
return await p;
|
||||
@ -3347,56 +3320,61 @@ export class Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
private async processTipImpl(tipToken: TipToken): Promise<TipRecord> {
|
||||
console.log("got tip token", tipToken);
|
||||
|
||||
const merchantDomain = new URI(tipToken.pickup_url).origin();
|
||||
|
||||
const deadlineSec = getTalerStampSec(tipToken.expiration);
|
||||
if (!deadlineSec) {
|
||||
throw Error("tipping failed (invalid expiration)");
|
||||
private async acceptTipImpl(
|
||||
tipId: string,
|
||||
merchantOrigin: string,
|
||||
): Promise<void> {
|
||||
let tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
|
||||
if (!tipRecord) {
|
||||
throw Error("tip not in database");
|
||||
}
|
||||
|
||||
let tipRecord = await this.q().get(Stores.tips, [
|
||||
tipToken.tip_id,
|
||||
merchantDomain,
|
||||
]);
|
||||
tipRecord.accepted = true;
|
||||
|
||||
if (tipRecord && tipRecord.pickedUp) {
|
||||
return tipRecord;
|
||||
// Create one transactional query, within this transaction
|
||||
// both the tip will be marked as accepted and coins
|
||||
// already withdrawn will be untainted.
|
||||
await this.q()
|
||||
.put(Stores.tips, tipRecord)
|
||||
.finish();
|
||||
|
||||
if (tipRecord.pickedUp) {
|
||||
console.log("tip already picked up");
|
||||
return;
|
||||
}
|
||||
const tipAmount = Amounts.parseOrThrow(tipToken.amount);
|
||||
await this.updateExchangeFromUrl(tipToken.exchange_url);
|
||||
await this.updateExchangeFromUrl(tipRecord.exchangeUrl);
|
||||
const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(
|
||||
tipToken.exchange_url,
|
||||
tipAmount,
|
||||
tipRecord.exchangeUrl,
|
||||
tipRecord.amount,
|
||||
);
|
||||
const planchets = await Promise.all(
|
||||
denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)),
|
||||
);
|
||||
const coinPubs: string[] = planchets.map(x => x.coinPub);
|
||||
const now = new Date().getTime();
|
||||
tipRecord = {
|
||||
accepted: false,
|
||||
amount: Amounts.parseOrThrow(tipToken.amount),
|
||||
coinPubs,
|
||||
deadline: deadlineSec,
|
||||
exchangeUrl: tipToken.exchange_url,
|
||||
merchantDomain,
|
||||
nextUrl: tipToken.next_url,
|
||||
pickedUp: false,
|
||||
planchets,
|
||||
timestamp: now,
|
||||
tipId: tipToken.tip_id,
|
||||
};
|
||||
|
||||
let merchantResp;
|
||||
if (!tipRecord.planchets) {
|
||||
const planchets = await Promise.all(
|
||||
denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)),
|
||||
);
|
||||
const coinPubs: string[] = planchets.map(x => x.coinPub);
|
||||
|
||||
tipRecord = await this.q().putOrGetExisting(Stores.tips, tipRecord, [
|
||||
tipRecord.tipId,
|
||||
merchantDomain,
|
||||
]);
|
||||
this.notifier.notify();
|
||||
await this.q().mutate(Stores.tips, [tipId, merchantOrigin], r => {
|
||||
if (!r.planchets) {
|
||||
r.planchets = planchets;
|
||||
r.coinPubs = coinPubs;
|
||||
}
|
||||
return r;
|
||||
});
|
||||
|
||||
this.notifier.notify();
|
||||
}
|
||||
|
||||
tipRecord = await this.q().get(Stores.tips, [tipId, merchantOrigin]);
|
||||
if (!tipRecord) {
|
||||
throw Error("tip not in database");
|
||||
}
|
||||
|
||||
if (!tipRecord.planchets) {
|
||||
throw Error("invariant violated");
|
||||
}
|
||||
|
||||
console.log("got planchets for tip!");
|
||||
|
||||
// Planchets in the form that the merchant expects
|
||||
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map(p => ({
|
||||
@ -3404,9 +3382,12 @@ export class Wallet {
|
||||
denom_pub_hash: p.denomPubHash,
|
||||
}));
|
||||
|
||||
let merchantResp;
|
||||
|
||||
try {
|
||||
const req = { planchets: planchetsDetail, tip_id: tipToken.tip_id };
|
||||
merchantResp = await this.http.postJson(tipToken.pickup_url, req);
|
||||
const req = { planchets: planchetsDetail, tip_id: tipId };
|
||||
merchantResp = await this.http.postJson(tipRecord.pickupUrl, req);
|
||||
console.log("got merchant resp:", merchantResp);
|
||||
} catch (e) {
|
||||
console.log("tipping failed", e);
|
||||
throw e;
|
||||
@ -3434,7 +3415,7 @@ export class Wallet {
|
||||
withdrawSig: response.reserve_sigs[i].reserve_sig,
|
||||
};
|
||||
await this.q().put(Stores.precoins, preCoin);
|
||||
this.processPreCoin(preCoin.coinPub);
|
||||
await this.processPreCoin(preCoin.coinPub);
|
||||
}
|
||||
|
||||
tipRecord.pickedUp = true;
|
||||
@ -3443,61 +3424,75 @@ export class Wallet {
|
||||
.put(Stores.tips, tipRecord)
|
||||
.finish();
|
||||
this.notifier.notify();
|
||||
|
||||
return tipRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start using the coins from a tip.
|
||||
*/
|
||||
async acceptTip(tipToken: TipToken): Promise<void> {
|
||||
const tipId = tipToken.tip_id;
|
||||
const merchantDomain = new URI(tipToken.pickup_url).origin();
|
||||
const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
|
||||
if (!tipRecord) {
|
||||
throw Error("tip not found");
|
||||
}
|
||||
tipRecord.accepted = true;
|
||||
|
||||
// Create one transactional query, within this transaction
|
||||
// both the tip will be marked as accepted and coins
|
||||
// already withdrawn will be untainted.
|
||||
const q = this.q();
|
||||
|
||||
q.put(Stores.tips, tipRecord);
|
||||
|
||||
const updateCoin = (c: CoinRecord) => {
|
||||
if (c.status === CoinStatus.TainedByTip) {
|
||||
c.status = CoinStatus.Fresh;
|
||||
}
|
||||
return c;
|
||||
};
|
||||
|
||||
for (const coinPub of tipRecord.coinPubs) {
|
||||
q.mutate(Stores.coins, coinPub, updateCoin);
|
||||
}
|
||||
|
||||
await q.finish();
|
||||
this.badge.showNotification();
|
||||
this.notifier.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
async getTipStatus(tipToken: TipToken): Promise<TipStatus> {
|
||||
const tipId = tipToken.tip_id;
|
||||
const merchantDomain = new URI(tipToken.pickup_url).origin();
|
||||
const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
|
||||
const amount = Amounts.parseOrThrow(tipToken.amount);
|
||||
const exchangeUrl = tipToken.exchange_url;
|
||||
this.processTip(tipToken);
|
||||
const nextUrl = tipToken.next_url;
|
||||
async getTipStatus(talerTipUri: string): Promise<TipStatus> {
|
||||
const res = parseTipUri(talerTipUri);
|
||||
if (!res) {
|
||||
throw Error("invalid taler://tip URI");
|
||||
}
|
||||
|
||||
const tipStatusUrl = new URI(res.tipPickupUrl)
|
||||
.addQuery({
|
||||
instance: res.merchantInstance,
|
||||
tip_id: res.tipId,
|
||||
})
|
||||
.href();
|
||||
console.log("checking tip status from", tipStatusUrl);
|
||||
const merchantResp = await this.http.get(tipStatusUrl);
|
||||
console.log("resp:", merchantResp.responseJson);
|
||||
const tipPickupStatus = TipPickupGetResponse.checked(
|
||||
merchantResp.responseJson,
|
||||
);
|
||||
|
||||
console.log("status", tipPickupStatus);
|
||||
|
||||
let amount = Amounts.parseOrThrow(tipPickupStatus.amount);
|
||||
|
||||
let tipRecord = await this.q().get(Stores.tips, [
|
||||
res.tipId,
|
||||
res.merchantOrigin,
|
||||
]);
|
||||
if (!tipRecord) {
|
||||
const withdrawDetails = await this.getWithdrawDetailsForAmount(
|
||||
tipPickupStatus.exchange_url,
|
||||
amount,
|
||||
);
|
||||
|
||||
tipRecord = {
|
||||
accepted: false,
|
||||
amount,
|
||||
coinPubs: [],
|
||||
deadline: getTalerStampSec(tipPickupStatus.stamp_expire)!,
|
||||
exchangeUrl: tipPickupStatus.exchange_url,
|
||||
merchantDomain: res.merchantOrigin,
|
||||
nextUrl: undefined,
|
||||
pickedUp: false,
|
||||
planchets: undefined,
|
||||
response: undefined,
|
||||
timestamp: new Date().getTime(),
|
||||
tipId: res.tipId,
|
||||
pickupUrl: res.tipPickupUrl,
|
||||
totalFees: Amounts.add(withdrawDetails.overhead, withdrawDetails.withdrawFee).amount,
|
||||
};
|
||||
await this.q().put(Stores.tips, tipRecord);
|
||||
}
|
||||
|
||||
const tipStatus: TipStatus = {
|
||||
accepted: !!tipRecord && tipRecord.accepted,
|
||||
amount,
|
||||
exchangeUrl,
|
||||
merchantDomain,
|
||||
nextUrl,
|
||||
tipRecord,
|
||||
amount: Amounts.parseOrThrow(tipPickupStatus.amount),
|
||||
amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
|
||||
exchangeUrl: tipPickupStatus.exchange_url,
|
||||
nextUrl: tipPickupStatus.extra.next_url,
|
||||
merchantOrigin: res.merchantOrigin,
|
||||
tipId: res.tipId,
|
||||
expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
|
||||
timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
|
||||
totalFees: tipRecord.totalFees,
|
||||
};
|
||||
|
||||
return tipStatus;
|
||||
}
|
||||
|
||||
@ -3526,11 +3521,6 @@ export class Wallet {
|
||||
const abortReq = { ...purchase.payReq, mode: "abort-refund" };
|
||||
|
||||
try {
|
||||
const config = {
|
||||
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
||||
timeout: 5000 /* 5 seconds */,
|
||||
validateStatus: (s: number) => s === 200,
|
||||
};
|
||||
resp = await this.http.postJson(purchase.contractTerms.pay_url, abortReq);
|
||||
} catch (e) {
|
||||
// Gives the user the option to retry / abort and refresh
|
||||
|
@ -427,10 +427,14 @@ export interface CoinWithDenom {
|
||||
export interface TipStatus {
|
||||
accepted: boolean;
|
||||
amount: AmountJson;
|
||||
amountLeft: AmountJson;
|
||||
nextUrl: string;
|
||||
merchantDomain: string;
|
||||
exchangeUrl: string;
|
||||
tipRecord?: TipRecord;
|
||||
tipId: string;
|
||||
merchantOrigin: string;
|
||||
expirationTimestamp: number;
|
||||
timestamp: number;
|
||||
totalFees: AmountJson;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -174,11 +174,11 @@ export interface MessageMap {
|
||||
response: AmountJson;
|
||||
};
|
||||
"accept-tip": {
|
||||
request: { tipToken: talerTypes.TipToken };
|
||||
response: walletTypes.TipStatus;
|
||||
request: { talerTipUri: string };
|
||||
response: void;
|
||||
};
|
||||
"get-tip-status": {
|
||||
request: { tipToken: talerTypes.TipToken };
|
||||
request: { talerTipUri: string };
|
||||
response: walletTypes.TipStatus;
|
||||
};
|
||||
"clear-notification": {
|
||||
|
@ -14,7 +14,6 @@
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Page shown to the user to confirm creation
|
||||
* of a reserve, usually requested by the bank.
|
||||
@ -28,152 +27,114 @@ import URI = require("urijs");
|
||||
|
||||
import * as i18n from "../../i18n";
|
||||
|
||||
import {
|
||||
acceptTip,
|
||||
getReserveCreationInfo,
|
||||
getTipStatus,
|
||||
} from "../wxApi";
|
||||
import { acceptTip, getReserveCreationInfo, getTipStatus } from "../wxApi";
|
||||
|
||||
import {
|
||||
WithdrawDetailView,
|
||||
renderAmount,
|
||||
} from "../renderHtml";
|
||||
import { WithdrawDetailView, renderAmount } from "../renderHtml";
|
||||
|
||||
import * as Amounts from "../../amounts";
|
||||
import { TipToken } from "../../talerTypes";
|
||||
import { ReserveCreationInfo, TipStatus } from "../../walletTypes";
|
||||
import { useState, useEffect } from "react";
|
||||
import { TipStatus } from "../../walletTypes";
|
||||
|
||||
interface TipDisplayProps {
|
||||
tipToken: TipToken;
|
||||
interface LoadingButtonProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
interface TipDisplayState {
|
||||
tipStatus?: TipStatus;
|
||||
rci?: ReserveCreationInfo;
|
||||
working: boolean;
|
||||
discarded: boolean;
|
||||
function LoadingButton(
|
||||
props:
|
||||
& React.PropsWithChildren<LoadingButtonProps>
|
||||
& React.DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
className="pure-button pure-button-primary"
|
||||
type="button"
|
||||
{...props}
|
||||
>
|
||||
{props.loading ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /></span> : null}
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
|
||||
constructor(props: TipDisplayProps) {
|
||||
super(props);
|
||||
this.state = { working: false, discarded: false };
|
||||
function TipDisplay(props: { talerTipUri: string }) {
|
||||
const [tipStatus, setTipStatus] = useState<TipStatus | undefined>(undefined);
|
||||
const [discarded, setDiscarded] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [finished, setFinished] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const doFetch = async () => {
|
||||
const ts = await getTipStatus(props.talerTipUri);
|
||||
setTipStatus(ts);
|
||||
};
|
||||
doFetch();
|
||||
}, []);
|
||||
|
||||
if (discarded) {
|
||||
return <span>You've discarded the tip.</span>;
|
||||
}
|
||||
|
||||
async update() {
|
||||
const tipStatus = await getTipStatus(this.props.tipToken);
|
||||
this.setState({ tipStatus });
|
||||
const rci = await getReserveCreationInfo(tipStatus.exchangeUrl, tipStatus.amount);
|
||||
this.setState({ rci });
|
||||
if (finished) {
|
||||
return <span>Tip has been accepted!</span>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.update();
|
||||
const port = chrome.runtime.connect();
|
||||
port.onMessage.addListener((msg: any) => {
|
||||
if (msg.notify) {
|
||||
console.log("got notified");
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
this.update();
|
||||
if (!tipStatus) {
|
||||
return <span>Loading ...</span>;
|
||||
}
|
||||
|
||||
renderExchangeInfo() {
|
||||
const rci = this.state.rci;
|
||||
if (!rci) {
|
||||
return <p>Waiting for info about exchange ...</p>;
|
||||
}
|
||||
const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
The tip is handled by the exchange <strong>{rci.exchangeInfo.baseUrl}</strong>.{" "}
|
||||
The exchange provider will charge
|
||||
{" "}
|
||||
<strong>{renderAmount(totalCost)}</strong>
|
||||
{" "}.
|
||||
</p>
|
||||
<WithdrawDetailView rci={rci} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const discard = () => {
|
||||
setDiscarded(true);
|
||||
};
|
||||
|
||||
accept() {
|
||||
this.setState({ working: true});
|
||||
acceptTip(this.props.tipToken);
|
||||
}
|
||||
const accept = async () => {
|
||||
setLoading(true);
|
||||
await acceptTip(props.talerTipUri);
|
||||
setFinished(true);
|
||||
};
|
||||
|
||||
discard() {
|
||||
this.setState({ discarded: true });
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const ts = this.state.tipStatus;
|
||||
if (!ts) {
|
||||
return <p>Processing ...</p>;
|
||||
}
|
||||
|
||||
const renderAccepted = () => (
|
||||
<>
|
||||
<p>You've accepted this tip! <a href={ts.nextUrl}>Go back to merchant</a></p>
|
||||
{this.renderExchangeInfo()}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderButtons = () => (
|
||||
<>
|
||||
return (
|
||||
<div>
|
||||
<h2>Tip Received!</h2>
|
||||
<p>
|
||||
You received a tip of <strong>{renderAmount(tipStatus.amount)}</strong>{" "}
|
||||
from <span> </span>
|
||||
<strong>{tipStatus.merchantOrigin}</strong>.
|
||||
</p>
|
||||
<p>
|
||||
The tip is handled by the exchange{" "}
|
||||
<strong>{tipStatus.exchangeUrl}</strong>. This exchange will charge fees
|
||||
of <strong>{renderAmount(tipStatus.totalFees)}</strong> for this
|
||||
operation.
|
||||
</p>
|
||||
<form className="pure-form">
|
||||
<button
|
||||
className="pure-button pure-button-primary"
|
||||
type="button"
|
||||
disabled={!(this.state.rci && this.state.tipStatus && this.state.tipStatus.tipRecord)}
|
||||
onClick={() => this.accept()}>
|
||||
{ this.state.working
|
||||
? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span>
|
||||
: null }
|
||||
Accept tip
|
||||
</button>
|
||||
<LoadingButton loading={loading} onClick={() => accept()}>
|
||||
AcceptTip
|
||||
</LoadingButton>
|
||||
{" "}
|
||||
<button className="pure-button" type="button" onClick={() => this.discard()}>
|
||||
<button className="pure-button" type="button" onClick={() => discard()}>
|
||||
Discard tip
|
||||
</button>
|
||||
</form>
|
||||
{ this.renderExchangeInfo() }
|
||||
</>
|
||||
);
|
||||
|
||||
const renderDiscarded = () => (
|
||||
<p>You've discarded this tip. <a href={ts.nextUrl}>Go back to merchant.</a></p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Tip Received!</h2>
|
||||
<p>You received a tip of <strong>{renderAmount(ts.amount)}</strong> from <span> </span>
|
||||
<strong>{ts.merchantDomain}</strong>.</p>
|
||||
{
|
||||
this.state.discarded
|
||||
? renderDiscarded()
|
||||
: ts.accepted
|
||||
? renderAccepted()
|
||||
: renderButtons()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const url = new URI(document.location.href);
|
||||
const query: any = URI.parseQuery(url.query());
|
||||
const talerTipUri = query.talerTipUri;
|
||||
if (typeof talerTipUri !== "string") {
|
||||
throw Error("talerTipUri must be a string");
|
||||
}
|
||||
|
||||
const tipToken = TipToken.checked(JSON.parse(query.tip_token));
|
||||
|
||||
ReactDOM.render(<TipDisplay tipToken={tipToken} />,
|
||||
document.getElementById("container")!);
|
||||
|
||||
ReactDOM.render(
|
||||
<TipDisplay talerTipUri={talerTipUri} />,
|
||||
document.getElementById("container")!,
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: provide more context information, maybe factor it out into a
|
||||
// TODO:generic error reporting function or component.
|
||||
|
@ -45,7 +45,6 @@ import {
|
||||
|
||||
import {
|
||||
MerchantRefundPermission,
|
||||
TipToken,
|
||||
} from "../talerTypes";
|
||||
|
||||
import { MessageMap, MessageType } from "./messages";
|
||||
@ -349,15 +348,15 @@ export function getFullRefundFees(args: { refundPermissions: MerchantRefundPermi
|
||||
/**
|
||||
* Get the status of processing a tip.
|
||||
*/
|
||||
export function getTipStatus(tipToken: TipToken): Promise<TipStatus> {
|
||||
return callBackend("get-tip-status", { tipToken });
|
||||
export function getTipStatus(talerTipUri: string): Promise<TipStatus> {
|
||||
return callBackend("get-tip-status", { talerTipUri });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a tip as accepted by the user.
|
||||
*/
|
||||
export function acceptTip(tipToken: TipToken): Promise<TipStatus> {
|
||||
return callBackend("accept-tip", { tipToken });
|
||||
export function acceptTip(talerTipUri: string): Promise<void> {
|
||||
return callBackend("accept-tip", { talerTipUri });
|
||||
}
|
||||
|
||||
|
||||
@ -423,4 +422,4 @@ export function preparePay(talerPayUri: string) {
|
||||
*/
|
||||
export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) {
|
||||
return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange });
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,6 @@ import * as wxApi from "./wxApi";
|
||||
import URI = require("urijs");
|
||||
import Port = chrome.runtime.Port;
|
||||
import MessageSender = chrome.runtime.MessageSender;
|
||||
import { TipToken } from "../talerTypes";
|
||||
import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi";
|
||||
|
||||
const NeedsWallet = Symbol("NeedsWallet");
|
||||
@ -182,7 +181,7 @@ function handleMessage(
|
||||
return Promise.resolve({ error: "bad url" });
|
||||
}
|
||||
const amount = AmountJson.checked(detail.amount);
|
||||
return needsWallet().getReserveCreationInfo(detail.baseUrl, amount);
|
||||
return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount);
|
||||
}
|
||||
case "get-history": {
|
||||
// TODO: limit history length
|
||||
@ -295,12 +294,10 @@ function handleMessage(
|
||||
case "accept-refund":
|
||||
return needsWallet().acceptRefund(detail.refundUrl);
|
||||
case "get-tip-status": {
|
||||
const tipToken = TipToken.checked(detail.tipToken);
|
||||
return needsWallet().getTipStatus(tipToken);
|
||||
return needsWallet().getTipStatus(detail.talerTipUri);
|
||||
}
|
||||
case "accept-tip": {
|
||||
const tipToken = TipToken.checked(detail.tipToken);
|
||||
return needsWallet().acceptTip(tipToken);
|
||||
return needsWallet().acceptTip(detail.talerTipUri);
|
||||
}
|
||||
case "clear-notification": {
|
||||
return needsWallet().clearNotification();
|
||||
@ -340,7 +337,7 @@ function handleMessage(
|
||||
return needsWallet().benchmarkCrypto(detail.repetitions);
|
||||
}
|
||||
case "get-withdraw-details": {
|
||||
return needsWallet().getWithdrawDetails(
|
||||
return needsWallet().getWithdrawDetailsForUri(
|
||||
detail.talerWithdrawUri,
|
||||
detail.maybeSelectedExchange,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user