From 165486a11268ab3d8009506916cd22d233cd248b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 7 Dec 2019 18:42:18 +0100 Subject: [PATCH] auto-refund --- src/dbTypes.ts | 7 ++++++- src/util/helpers.ts | 38 +++++++++++++++++++++++------------ src/wallet-impl/pay.ts | 45 +++++++++++++++++++++++++++++++++++++----- src/wallet-impl/tip.ts | 4 ++-- 4 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/dbTypes.ts b/src/dbTypes.ts index 553040614..3ffade4e3 100644 --- a/src/dbTypes.ts +++ b/src/dbTypes.ts @@ -788,7 +788,7 @@ export interface TipRecord { /** * Timestamp, the tip can't be picked up anymore after this deadline. */ - deadline: number; + deadline: Timestamp; /** * The exchange that will sign our coins, chosen by the merchant. @@ -1066,6 +1066,11 @@ export interface PurchaseRecord { * Last error (or undefined) for querying the refund status with the merchant. */ lastRefundApplyError: OperationError | undefined; + + /** + * Continue querying the refund status until this deadline has expired. + */ + autoRefundDeadline: Timestamp | undefined; } /** diff --git a/src/util/helpers.ts b/src/util/helpers.ts index eb8a1c7b2..3831e84af 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -24,7 +24,7 @@ import { AmountJson } from "./amounts"; import * as Amounts from "./amounts"; -import { Timestamp } from "../walletTypes"; +import { Timestamp, Duration } from "../walletTypes"; /** * Show an amount in a form suitable for the user. @@ -151,6 +151,30 @@ export function extractTalerStampOrThrow(stamp: string): Timestamp { return r; } +/** + * Extract a duration from a Taler duration string. + */ +export function extractTalerDuration(duration: string): Duration | undefined { + const m = duration.match(/\/?Delay\(([0-9]*)\)\/?/); + if (!m || !m[1]) { + return undefined; + } + return { + d_ms: parseInt(m[1], 10) * 1000, + }; +} + +/** + * Extract a duration from a Taler duration string. + */ +export function extractTalerDurationOrThrow(duration: string): Duration { + const r = extractTalerDuration(duration); + if (!r) { + throw Error("invalid duration"); + } + return r; +} + /** * Check if a timestamp is in the right format. */ @@ -159,18 +183,6 @@ export function timestampCheck(stamp: string): boolean { } -/** - * Get a JavaScript Date object from a Taler date string. - * Returns null if input is not in the right format. - */ -export function getTalerStampDate(stamp: string): Date | null { - const sec = getTalerStampSec(stamp); - if (sec == null) { - return null; - } - return new Date(sec * 1000); -} - /** * Compute the hash function of a JSON object. */ diff --git a/src/wallet-impl/pay.ts b/src/wallet-impl/pay.ts index 7076f905d..c39feeec3 100644 --- a/src/wallet-impl/pay.ts +++ b/src/wallet-impl/pay.ts @@ -62,6 +62,8 @@ import { strcmp, canonicalJson, extractTalerStampOrThrow, + extractTalerDurationOrThrow, + extractTalerDuration, } from "../util/helpers"; import { Logger } from "../util/logging"; import { InternalWalletState } from "./state"; @@ -359,6 +361,7 @@ async function recordConfirmPay( lastRefundApplyError: undefined, refundApplyRetryInfo: initRetryInfo(), firstSuccessfulPayTimestamp: undefined, + autoRefundDeadline: undefined, }; await runWithWriteTransaction( @@ -704,9 +707,23 @@ export async function submitPay( // FIXME: properly display error throw Error("merchant payment signature invalid"); } + const isFirst = purchase.firstSuccessfulPayTimestamp === undefined; purchase.firstSuccessfulPayTimestamp = getTimestampNow(); purchase.lastPayError = undefined; purchase.payRetryInfo = initRetryInfo(false); + if (isFirst) { + const ar = purchase.contractTerms.auto_refund; + if (ar) { + const autoRefundDelay = extractTalerDuration(ar); + if (autoRefundDelay) { + purchase.refundStatusRequested = true; + purchase.autoRefundDeadline = { + t_ms: getTimestampNow().t_ms + autoRefundDelay.d_ms, + } + } + } + } + const modifiedCoins: CoinRecord[] = []; for (const pc of purchase.payReq.coins) { const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub); @@ -1064,11 +1081,6 @@ async function acceptRefundResponse( return; } - p.lastRefundStatusTimestamp = getTimestampNow(); - p.lastRefundStatusError = undefined; - p.refundStatusRetryInfo = initRetryInfo(); - p.refundStatusRequested = false; - for (const perm of refundPermissions) { if ( !p.refundsPending[perm.merchant_sig] && @@ -1079,6 +1091,29 @@ async function acceptRefundResponse( } } + // Are we done with querying yet, or do we need to do another round + // after a retry delay? + let queryDone = true; + + if (numNewRefunds === 0) { + if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms < getTimestampNow().t_ms) { + queryDone = false; + } + } + + if (queryDone) { + p.lastRefundStatusTimestamp = getTimestampNow(); + p.lastRefundStatusError = undefined; + p.refundStatusRetryInfo = initRetryInfo(); + p.refundStatusRequested = false; + } else { + // No error, but we need to try again! + p.lastRefundStatusTimestamp = getTimestampNow(); + p.refundStatusRetryInfo.retryCounter++; + updateRetryInfoTimeout(p.refundStatusRetryInfo); + p.lastRefundStatusError = undefined; + } + if (numNewRefunds) { p.lastRefundApplyError = undefined; p.refundApplyRetryInfo = initRetryInfo(); diff --git a/src/wallet-impl/tip.ts b/src/wallet-impl/tip.ts index 9cfaed930..11e029fcd 100644 --- a/src/wallet-impl/tip.ts +++ b/src/wallet-impl/tip.ts @@ -23,7 +23,7 @@ import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTy import * as Amounts from "../util/amounts"; import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes"; import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw"; -import { getTalerStampSec } from "../util/helpers"; +import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers"; import { updateExchangeFromUrl } from "./exchanges"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { guardOperationException } from "./errors"; @@ -68,7 +68,7 @@ export async function getTipStatus( tipId, accepted: false, amount, - deadline: getTalerStampSec(tipPickupStatus.stamp_expire)!, + deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire), exchangeUrl: tipPickupStatus.exchange_url, merchantBaseUrl: res.merchantBaseUrl, nextUrl: undefined,