support for tipping protocol changes

This commit is contained in:
Florian Dold 2019-08-30 17:27:59 +02:00
parent defbf625bd
commit 5ec344290e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
10 changed files with 445 additions and 426 deletions

View File

@ -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();

View File

@ -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);

View File

@ -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;
}

View File

@ -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(),
};
}

View File

@ -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

View File

@ -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;
} }
/** /**

View File

@ -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": {

View File

@ -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.

View File

@ -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 });
} }

View File

@ -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,
); );