From b063382d25d1ed8572ebe2f52bf54247379300d5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 8 Sep 2020 17:40:47 +0530 Subject: tipping API and integration test --- packages/taler-wallet-core/src/db.ts | 2 +- .../taler-wallet-core/src/operations/pending.ts | 2 +- packages/taler-wallet-core/src/operations/tip.ts | 81 +++++++++------------- packages/taler-wallet-core/src/types/dbTypes.ts | 4 +- .../taler-wallet-core/src/types/notifications.ts | 1 + packages/taler-wallet-core/src/types/talerTypes.ts | 17 ++--- .../taler-wallet-core/src/types/walletTypes.ts | 56 +++++++++++---- packages/taler-wallet-core/src/wallet.ts | 19 +++-- 8 files changed, 100 insertions(+), 82 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index b3203935e..c21ff4a43 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -8,7 +8,7 @@ import { IDBFactory, IDBDatabase } from "idb-bridge"; * with each major change. When incrementing the major version, * the wallet should import data from the previous version. */ -const TALER_DB_NAME = "taler-walletdb-v10"; +const TALER_DB_NAME = "taler-walletdb-v11"; /** * Current database minor version, should be incremented diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index 3c631eb77..91a55c705 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -368,7 +368,7 @@ async function gatherTipPending( type: PendingOperationType.TipPickup, givesLifeness: true, merchantBaseUrl: tip.merchantBaseUrl, - tipId: tip.tipId, + tipId: tip.walletTipId, merchantTipId: tip.merchantTipId, }); } diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 7949648c5..6fe374bf0 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -16,7 +16,7 @@ import { InternalWalletState } from "./state"; import { parseTipUri } from "../util/taleruri"; -import { TipStatus, TalerErrorDetails } from "../types/walletTypes"; +import { PrepareTipResult, TalerErrorDetails } from "../types/walletTypes"; import { TipPlanchetDetail, codecForTipPickupGetResponse, @@ -46,20 +46,23 @@ import { getTimestampNow } from "../util/time"; import { readSuccessResponseJsonOrThrow } from "../util/http"; import { URL } from "../util/url"; import { Logger } from "../util/logging"; +import { checkDbInvariant } from "../util/invariants"; const logger = new Logger("operations/tip.ts"); -export async function getTipStatus( +export async function prepareTip( ws: InternalWalletState, talerTipUri: string, -): Promise { +): Promise { const res = parseTipUri(talerTipUri); if (!res) { throw Error("invalid taler://tip URI"); } - const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl); - tipStatusUrl.searchParams.set("tip_id", res.merchantTipId); + const tipStatusUrl = new URL( + `tips/${res.merchantTipId}`, + res.merchantBaseUrl, + ); logger.trace("checking tip status from", tipStatusUrl.href); const merchantResp = await ws.http.get(tipStatusUrl.href); const tipPickupStatus = await readSuccessResponseJsonOrThrow( @@ -68,7 +71,7 @@ export async function getTipStatus( ); logger.trace(`status ${tipPickupStatus}`); - const amount = Amounts.parseOrThrow(tipPickupStatus.amount); + const amount = Amounts.parseOrThrow(tipPickupStatus.tip_amount); const merchantOrigin = new URL(res.merchantBaseUrl).origin; @@ -85,7 +88,7 @@ export async function getTipStatus( amount, ); - const tipId = encodeCrock(getRandomBytes(32)); + const walletTipId = encodeCrock(getRandomBytes(32)); const selectedDenoms = await selectWithdrawalDenoms( ws, tipPickupStatus.exchange_url, @@ -93,11 +96,11 @@ export async function getTipStatus( ); tipRecord = { - tipId, + walletTipId: walletTipId, acceptedTimestamp: undefined, rejectedTimestamp: undefined, amount, - deadline: tipPickupStatus.stamp_expire, + deadline: tipPickupStatus.expiration, exchangeUrl: tipPickupStatus.exchange_url, merchantBaseUrl: res.merchantBaseUrl, nextUrl: undefined, @@ -117,18 +120,13 @@ export async function getTipStatus( await ws.db.put(Stores.tips, tipRecord); } - const tipStatus: TipStatus = { + const tipStatus: PrepareTipResult = { accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, - amount: Amounts.parseOrThrow(tipPickupStatus.amount), - amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left), - exchangeUrl: tipPickupStatus.exchange_url, - nextUrl: tipPickupStatus.extra.next_url, - merchantOrigin: merchantOrigin, - merchantTipId: res.merchantTipId, - expirationTimestamp: tipPickupStatus.stamp_expire, - timestamp: tipPickupStatus.stamp_created, - totalFees: tipRecord.totalFees, - tipId: tipRecord.tipId, + amount: Amounts.stringify(tipPickupStatus.tip_amount), + exchangeBaseUrl: tipPickupStatus.exchange_url, + expirationTimestamp: tipPickupStatus.expiration, + totalFees: Amounts.stringify(tipRecord.totalFees), + walletTipId: tipRecord.walletTipId, }; return tipStatus; @@ -152,7 +150,9 @@ async function incrementTipRetry( t.lastError = err; await tx.put(Stores.tips, t); }); - ws.notify({ type: NotificationType.TipOperationError }); + if (err) { + ws.notify({ type: NotificationType.TipOperationError, error: err }); + } } export async function processTip( @@ -225,15 +225,8 @@ async function processTipImpl( } tipRecord = await ws.db.get(Stores.tips, tipId); - if (!tipRecord) { - throw Error("tip not in database"); - } - - if (!tipRecord.planchets) { - throw Error("invariant violated"); - } - - logger.trace("got planchets for tip!"); + checkDbInvariant(!!tipRecord, "tip record should be in database"); + checkDbInvariant(!!tipRecord.planchets, "tip record should have planchets"); // Planchets in the form that the merchant expects const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({ @@ -241,23 +234,17 @@ async function processTipImpl( denom_pub_hash: p.denomPubHash, })); - let merchantResp; - - const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl); - - try { - const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId }; - merchantResp = await ws.http.postJson(tipStatusUrl.href, req); - if (merchantResp.status !== 200) { - throw Error(`unexpected status ${merchantResp.status} for tip-pickup`); - } - logger.trace("got merchant resp:", merchantResp); - } catch (e) { - logger.warn("tipping failed", e); - throw e; - } + const tipStatusUrl = new URL( + `/tips/${tipRecord.merchantTipId}/pickup`, + tipRecord.merchantBaseUrl, + ); - const response = codecForTipResponse().decode(await merchantResp.json()); + const req = { planchets: planchetsDetail }; + const merchantResp = await ws.http.postJson(tipStatusUrl.href, req); + const response = await readSuccessResponseJsonOrThrow( + merchantResp, + codecForTipResponse(), + ); if (response.reserve_sigs.length !== tipRecord.planchets.length) { throw Error("number of tip responses does not match requested planchets"); @@ -293,7 +280,7 @@ async function processTipImpl( exchangeBaseUrl: tipRecord.exchangeUrl, source: { type: WithdrawalSourceType.Tip, - tipId: tipRecord.tipId, + tipId: tipRecord.walletTipId, }, timestampStart: getTimestampNow(), withdrawalGroupId: withdrawalGroupId, diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts index 0ee41a6a5..4e2ba1bb4 100644 --- a/packages/taler-wallet-core/src/types/dbTypes.ts +++ b/packages/taler-wallet-core/src/types/dbTypes.ts @@ -986,7 +986,7 @@ export interface TipRecord { /** * Tip ID chosen by the wallet. */ - tipId: string; + walletTipId: string; /** * The merchant's identifier for this tip. @@ -1760,7 +1760,7 @@ class ReserveHistoryStore extends Store { class TipsStore extends Store { constructor() { - super("tips", { keyPath: "tipId" }); + super("tips", { keyPath: "walletTipId" }); } } diff --git a/packages/taler-wallet-core/src/types/notifications.ts b/packages/taler-wallet-core/src/types/notifications.ts index 7a51f0d83..e1b9a7aff 100644 --- a/packages/taler-wallet-core/src/types/notifications.ts +++ b/packages/taler-wallet-core/src/types/notifications.ts @@ -186,6 +186,7 @@ export interface ProposalOperationErrorNotification { export interface TipOperationErrorNotification { type: NotificationType.TipOperationError; + error: TalerErrorDetails; } export interface WithdrawOperationErrorNotification { diff --git a/packages/taler-wallet-core/src/types/talerTypes.ts b/packages/taler-wallet-core/src/types/talerTypes.ts index c944f1561..52dc4cb62 100644 --- a/packages/taler-wallet-core/src/types/talerTypes.ts +++ b/packages/taler-wallet-core/src/types/talerTypes.ts @@ -773,17 +773,11 @@ export class WithdrawOperationStatusResponse { * Response from the merchant. */ export class TipPickupGetResponse { - extra: any; - - amount: string; - - amount_left: string; + tip_amount: string; exchange_url: string; - stamp_expire: Timestamp; - - stamp_created: Timestamp; + expiration: Timestamp; } export class WithdrawResponse { @@ -1261,12 +1255,9 @@ export const codecForWithdrawOperationStatusResponse = (): Codec< export const codecForTipPickupGetResponse = (): Codec => buildCodecForObject() - .property("extra", codecForAny()) - .property("amount", codecForString()) - .property("amount_left", codecForString()) + .property("tip_amount", codecForString()) .property("exchange_url", codecForString()) - .property("stamp_expire", codecForTimestamp) - .property("stamp_created", codecForTimestamp) + .property("expiration", codecForTimestamp) .build("TipPickupGetResponse"); export const codecForRecoupConfirmation = (): Codec => diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index 82f29c39d..fb049caf9 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -38,7 +38,7 @@ import { ExchangeWireInfo, DenominationSelectionInfo, } from "./dbTypes"; -import { Timestamp } from "../util/time"; +import { Timestamp, codecForTimestamp } from "../util/time"; import { buildCodecForObject, codecForString, @@ -348,23 +348,33 @@ export class ReturnCoinsRequest { static checked: (obj: any) => ReturnCoinsRequest; } -/** - * Status of processing a tip. - */ -export interface TipStatus { +export interface PrepareTipResult { + /** + * Unique ID for the tip assigned by the wallet. + * Typically different from the merchant-generated tip ID. + */ + walletTipId: string; + + /** + * Has the tip already been accepted? + */ accepted: boolean; - amount: AmountJson; - amountLeft: AmountJson; - nextUrl: string; - exchangeUrl: string; - tipId: string; - merchantTipId: string; - merchantOrigin: string; + amount: AmountString; + totalFees: AmountString; + exchangeBaseUrl: string; expirationTimestamp: Timestamp; - timestamp: Timestamp; - totalFees: AmountJson; } +export const codecForPrepareTipResult = (): Codec => + buildCodecForObject() + .property("accepted", codecForBoolean()) + .property("amount", codecForAmountString()) + .property("totalFees", codecForAmountString()) + .property("exchangeBaseUrl", codecForString()) + .property("expirationTimestamp", codecForTimestamp) + .property("walletTipId", codecForString()) + .build("PrepareTipResult"); + export interface BenchmarkResult { time: { [s: string]: number }; repetitions: number; @@ -903,3 +913,21 @@ export const codecForForceRefreshRequest = (): Codec => buildCodecForObject() .property("coinPubList", codecForList(codecForString())) .build("ForceRefreshRequest"); + +export interface PrepareTipRequest { + talerTipUri: string; +} + +export const codecForPrepareTipRequest = (): Codec => + buildCodecForObject() + .property("talerTipUri", codecForString()) + .build("PrepareTipRequest"); + +export interface AcceptTipRequest { + walletTipId: string; +} + +export const codecForAcceptTipRequest = (): Codec => + buildCodecForObject() + .property("walletTipId", codecForString()) + .build("AcceptTipRequest"); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 9666665a4..0507ac8b2 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -59,7 +59,6 @@ import { ConfirmPayResult, ReturnCoinsRequest, SenderWireInfos, - TipStatus, PreparePayResult, AcceptWithdrawalResponse, PurchaseDetails, @@ -93,6 +92,9 @@ import { codecForSetCoinSuspendedRequest, codecForForceExchangeUpdateRequest, codecForForceRefreshRequest, + PrepareTipResult, + codecForPrepareTipRequest, + codecForAcceptTipRequest, } from "./types/walletTypes"; import { Logger } from "./util/logging"; @@ -121,7 +123,7 @@ import { import { processWithdrawGroup } from "./operations/withdraw"; import { getPendingOperations } from "./operations/pending"; import { getBalances } from "./operations/balance"; -import { acceptTip, getTipStatus, processTip } from "./operations/tip"; +import { acceptTip, prepareTip, processTip } from "./operations/tip"; import { TimerGroup } from "./util/timer"; import { AsyncCondition } from "./util/promiseUtils"; import { AsyncOpMemoSingle } from "./util/asyncMemo"; @@ -769,8 +771,8 @@ export class Wallet { } } - async getTipStatus(talerTipUri: string): Promise { - return getTipStatus(this.ws, talerTipUri); + async prepareTip(talerTipUri: string): Promise { + return prepareTip(this.ws, talerTipUri); } async abortFailedPayment(contractTermsHash: string): Promise { @@ -1096,6 +1098,15 @@ export class Wallet { refreshGroupId, }; } + case "prepareTip": { + const req = codecForPrepareTipRequest().decode(payload); + return await this.prepareTip(req.talerTipUri); + } + case "acceptTip": { + const req = codecForAcceptTipRequest().decode(payload); + await this.acceptTip(req.walletTipId); + return {}; + } } throw OperationFailedError.fromCode( TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, -- cgit v1.2.3