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