From f57dc7bf7a1e3a14c67512ba67d92fa350c95c0e Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 10 Jun 2022 13:03:47 +0200 Subject: wallet-core: implement and test forced coin/denom selection --- packages/taler-wallet-core/src/db.ts | 2 +- .../src/operations/backup/import.ts | 3 +- .../taler-wallet-core/src/operations/deposits.ts | 3 +- packages/taler-wallet-core/src/operations/pay.ts | 37 ++++-- .../taler-wallet-core/src/operations/reserves.ts | 108 +++++++++------- .../taler-wallet-core/src/operations/testing.ts | 76 ++++++++---- .../src/util/coinSelection.test.ts | 2 + .../taler-wallet-core/src/util/coinSelection.ts | 138 ++++++++++++++++----- packages/taler-wallet-core/src/util/retries.ts | 2 +- packages/taler-wallet-core/src/wallet-api-types.ts | 11 +- packages/taler-wallet-core/src/wallet.ts | 78 ++++++------ 11 files changed, 302 insertions(+), 158 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 8fe1937aa..b22bc585e 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -41,9 +41,9 @@ import { TalerProtocolTimestamp, TalerProtocolDuration, AgeCommitmentProof, + PayCoinSelection, } from "@gnu-taler/taler-util"; import { RetryInfo } from "./util/retries.js"; -import { PayCoinSelection } from "./util/coinSelection.js"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; /** diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 16a88fe7c..3a9121502 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -18,7 +18,7 @@ import { AmountJson, Amounts, BackupCoinSourceType, BackupDenomSel, BackupProposalStatus, BackupPurchase, BackupRefreshReason, BackupRefundState, codecForContractTerms, - DenomKeyType, j2s, Logger, RefreshReason, TalerProtocolTimestamp, + DenomKeyType, j2s, Logger, PayCoinSelection, RefreshReason, TalerProtocolTimestamp, WalletBackupContentV1 } from "@gnu-taler/taler-util"; import { @@ -29,7 +29,6 @@ import { ReserveRecordStatus, WalletContractData, WalletRefundItem, WalletStoresV1, WireInfo } from "../../db.js"; import { InternalWalletState } from "../../internal-wallet-state.js"; -import { PayCoinSelection } from "../../util/coinSelection.js"; import { checkDbInvariant, checkLogicInvariant diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 41f051cb3..a016cb8e5 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -35,6 +35,7 @@ import { Logger, NotificationType, parsePaytoUri, + PayCoinSelection, PrepareDepositRequest, PrepareDepositResponse, TalerErrorDetail, @@ -45,7 +46,7 @@ import { } from "@gnu-taler/taler-util"; import { DepositGroupRecord, OperationStatus, WireFee } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; -import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js"; +import { selectPayCoins } from "../util/coinSelection.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import { RetryInfo } from "../util/retries.js"; import { guardOperationException } from "./common.js"; diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index f22d51a9d..b6bae7518 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -40,12 +40,14 @@ import { durationMin, durationMul, encodeCrock, + ForcedCoinSel, getRandomBytes, HttpStatusCode, j2s, Logger, NotificationType, parsePayUri, + PayCoinSelection, PreparePayResult, PreparePayResultType, RefreshReason, @@ -81,8 +83,8 @@ import { import { AvailableCoinInfo, CoinCandidateSelection, - PayCoinSelection, PreviousPayCoins, + selectForcedPayCoins, selectPayCoins, } from "../util/coinSelection.js"; import { ContractTermsUtil } from "../util/contractTerms.js"; @@ -305,6 +307,7 @@ export async function getCandidatePayCoins( } candidateCoins.push({ availableAmount: coin.currentAmount, + value: denom.value, coinPub: coin.coinPub, denomPub: denom.denomPub, feeDeposit: denom.feeDeposit, @@ -1423,6 +1426,7 @@ export async function confirmPay( ws: InternalWalletState, proposalId: string, sessionIdOverride?: string, + forcedCoinSel?: ForcedCoinSel, ): Promise { logger.trace( `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`, @@ -1479,15 +1483,28 @@ export async function confirmPay( wireMethod: contractData.wireMethod, }); - const res = selectPayCoins({ - candidates, - contractTermsAmount: contractData.amount, - depositFeeLimit: contractData.maxDepositFee, - wireFeeAmortization: contractData.wireFeeAmortization ?? 1, - wireFeeLimit: contractData.maxWireFee, - prevPayCoins: [], - requiredMinimumAge: contractData.minimumAge, - }); + let res: PayCoinSelection | undefined = undefined; + + if (forcedCoinSel) { + res = selectForcedPayCoins(forcedCoinSel, { + candidates, + contractTermsAmount: contractData.amount, + depositFeeLimit: contractData.maxDepositFee, + wireFeeAmortization: contractData.wireFeeAmortization ?? 1, + wireFeeLimit: contractData.maxWireFee, + requiredMinimumAge: contractData.minimumAge, + }); + } else { + res = selectPayCoins({ + candidates, + contractTermsAmount: contractData.amount, + depositFeeLimit: contractData.maxDepositFee, + wireFeeAmortization: contractData.wireFeeAmortization ?? 1, + wireFeeLimit: contractData.maxWireFee, + prevPayCoins: [], + requiredMinimumAge: contractData.minimumAge, + }); + } logger.trace("coin selection result", res); diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index d9fc8cf45..b33f574f4 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -15,6 +15,7 @@ */ import { + AbsoluteTime, AcceptWithdrawalResponse, addPaytoQueryParams, Amounts, @@ -28,6 +29,7 @@ import { durationMax, durationMin, encodeCrock, + ForcedDenomSel, getRandomBytes, j2s, Logger, @@ -35,13 +37,10 @@ import { randomBytes, TalerErrorCode, TalerErrorDetail, - AbsoluteTime, URL, - AmountString, - ForcedDenomSel, } from "@gnu-taler/taler-util"; -import { InternalWalletState } from "../internal-wallet-state.js"; import { + DenomSelectionState, OperationStatus, ReserveBankInfo, ReserveRecord, @@ -50,6 +49,7 @@ import { WithdrawalGroupRecord, } from "../db.js"; import { TalerError } from "../errors.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { readSuccessResponseJsonOrErrorCode, @@ -57,9 +57,8 @@ import { throwUnexpectedRequestError, } from "../util/http.js"; import { GetReadOnlyAccess } from "../util/query.js"; -import { - RetryInfo, -} from "../util/retries.js"; +import { RetryInfo } from "../util/retries.js"; +import { guardOperationException } from "./common.js"; import { getExchangeDetails, getExchangePaytoUri, @@ -70,10 +69,10 @@ import { getBankWithdrawalInfo, getCandidateWithdrawalDenoms, processWithdrawGroup, + selectForcedWithdrawalDenominations, selectWithdrawalDenominations, updateWithdrawalDenoms, } from "./withdraw.js"; -import { guardOperationException } from "./common.js"; const logger = new Logger("taler-wallet-core:reserves.ts"); @@ -178,7 +177,18 @@ export async function createReserve( await updateWithdrawalDenoms(ws, canonExchange); const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange); - const initialDenomSel = selectWithdrawalDenominations(req.amount, denoms); + + let initialDenomSel: DenomSelectionState; + if (req.forcedDenomSel) { + logger.warn("using forced denom selection"); + initialDenomSel = selectForcedWithdrawalDenominations( + req.amount, + denoms, + req.forcedDenomSel, + ); + } else { + initialDenomSel = selectWithdrawalDenominations(req.amount, denoms); + } const reserveRecord: ReserveRecord = { instructedAmount: req.amount, @@ -436,7 +446,7 @@ async function processReserveBankStatus( ); if (status.aborted) { - logger.trace("bank aborted the withdrawal"); + logger.info("bank aborted the withdrawal"); await ws.db .mktx((x) => ({ reserves: x.reserves, @@ -463,12 +473,14 @@ async function processReserveBankStatus( return; } - if (status.selection_done) { - if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) { - await registerReserveWithBank(ws, reservePub); - return await processReserveBankStatus(ws, reservePub); - } - } else { + // Bank still needs to know our reserve info + if (!status.selection_done) { + await registerReserveWithBank(ws, reservePub); + return await processReserveBankStatus(ws, reservePub); + } + + // FIXME: Why do we do this?! + if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) { await registerReserveWithBank(ws, reservePub); return await processReserveBankStatus(ws, reservePub); } @@ -482,29 +494,26 @@ async function processReserveBankStatus( if (!r) { return; } + // Re-check reserve status within transaction + switch (r.reserveStatus) { + case ReserveRecordStatus.RegisteringBank: + case ReserveRecordStatus.WaitConfirmBank: + break; + default: + return; + } if (status.transfer_done) { - switch (r.reserveStatus) { - case ReserveRecordStatus.RegisteringBank: - case ReserveRecordStatus.WaitConfirmBank: - break; - default: - return; - } const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); r.timestampBankConfirmed = now; r.reserveStatus = ReserveRecordStatus.QueryingStatus; r.operationStatus = OperationStatus.Pending; r.retryInfo = RetryInfo.reset(); } else { - switch (r.reserveStatus) { - case ReserveRecordStatus.WaitConfirmBank: - break; - default: - return; - } + logger.info("Withdrawal operation not yet confirmed by bank"); if (r.bankInfo) { r.bankInfo.confirmUrl = status.confirm_transfer_url; } + r.retryInfo = RetryInfo.increment(r.retryInfo); } await tx.reserves.put(r); }); @@ -540,6 +549,8 @@ async function updateReserve( const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl); reserveUrl.searchParams.set("timeout_ms", "30000"); + logger.info(`querying reserve status via ${reserveUrl}`); + const resp = await ws.http.get(reserveUrl.href, { timeout: getReserveRequestTimeout(reserve), }); @@ -553,7 +564,7 @@ async function updateReserve( if ( resp.status === 404 && result.talerErrorResponse.code === - TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN + TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN ) { ws.notify({ type: NotificationType.ReserveNotYetFound, @@ -589,6 +600,7 @@ async function updateReserve( if (!newReserve) { return; } + let amountReservePlus = reserveBalance; let amountReserveMinus = Amounts.getZero(currency); @@ -628,30 +640,33 @@ async function updateReserve( amountReservePlus, amountReserveMinus, ).amount; - const denomSel = selectWithdrawalDenominations(remainingAmount, denoms); - - logger.trace( - `Remaining unclaimed amount in reseve is ${Amounts.stringify( - remainingAmount, - )} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`, - ); - - if (denomSel.selectedDenoms.length === 0) { - newReserve.reserveStatus = ReserveRecordStatus.Dormant; - newReserve.operationStatus = OperationStatus.Finished; - delete newReserve.lastError; - delete newReserve.retryInfo; - await tx.reserves.put(newReserve); - return; - } let withdrawalGroupId: string; + let denomSel: DenomSelectionState; if (!newReserve.initialWithdrawalStarted) { withdrawalGroupId = newReserve.initialWithdrawalGroupId; newReserve.initialWithdrawalStarted = true; + denomSel = newReserve.initialDenomSel; } else { withdrawalGroupId = encodeCrock(randomBytes(32)); + + denomSel = selectWithdrawalDenominations(remainingAmount, denoms); + + logger.trace( + `Remaining unclaimed amount in reseve is ${Amounts.stringify( + remainingAmount, + )} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`, + ); + + if (denomSel.selectedDenoms.length === 0) { + newReserve.reserveStatus = ReserveRecordStatus.Dormant; + newReserve.operationStatus = OperationStatus.Finished; + delete newReserve.lastError; + delete newReserve.retryInfo; + await tx.reserves.put(newReserve); + return; + } } const withdrawalRecord: WithdrawalGroupRecord = { @@ -768,6 +783,7 @@ export async function createTalerWithdrawReserve( senderWire: withdrawInfo.senderWire, exchangePaytoUri: exchangePaytoUri, restrictAge: options.restrictAge, + forcedDenomSel: options.forcedDenomSel, }); // We do this here, as the reserve should be registered before we return, // so that we can redirect the user to the bank's status page. diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 555e2d73d..d609011ca 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -17,7 +17,12 @@ /** * Imports. */ -import { Logger } from "@gnu-taler/taler-util"; +import { + ConfirmPayResultType, + Logger, + TestPayResult, + WithdrawTestBalanceRequest, +} from "@gnu-taler/taler-util"; import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, @@ -39,6 +44,7 @@ import { InternalWalletState } from "../internal-wallet-state.js"; import { confirmPay, preparePayForUri } from "./pay.js"; import { getBalances } from "./balance.js"; import { applyRefund } from "./refund.js"; +import { checkLogicInvariant } from "../util/invariants.js"; const logger = new Logger("operations/testing.ts"); @@ -82,10 +88,12 @@ function makeBasicAuthHeader(username: string, password: string): string { export async function withdrawTestBalance( ws: InternalWalletState, - amount = "TESTKUDOS:10", - bankBaseUrl = "https://bank.test.taler.net/", - exchangeBaseUrl = "https://exchange.test.taler.net/", + req: WithdrawTestBalanceRequest, ): Promise { + const bankBaseUrl = req.bankBaseUrl; + const amount = req.amount; + const exchangeBaseUrl = req.exchangeBaseUrl; + const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl); logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`); @@ -100,6 +108,9 @@ export async function withdrawTestBalance( ws, wresp.taler_withdraw_uri, exchangeBaseUrl, + { + forcedDenomSel: req.forcedDenomSel, + }, ); await confirmBankWithdrawalUri( @@ -140,7 +151,10 @@ export async function createDemoBankWithdrawalUri( }, { headers: { - Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password), + Authorization: makeBasicAuthHeader( + bankUser.username, + bankUser.password, + ), }, }, ); @@ -163,7 +177,10 @@ async function confirmBankWithdrawalUri( {}, { headers: { - Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password), + Authorization: makeBasicAuthHeader( + bankUser.username, + bankUser.password, + ), }, }, ); @@ -331,12 +348,11 @@ export async function runIntegrationTest( const currency = parsedSpendAmount.currency; logger.info("withdrawing test balance"); - await withdrawTestBalance( - ws, - args.amountToWithdraw, - args.bankBaseUrl, - args.exchangeBaseUrl, - ); + await withdrawTestBalance(ws, { + amount: args.amountToWithdraw, + bankBaseUrl: args.bankBaseUrl, + exchangeBaseUrl: args.exchangeBaseUrl, + }); await ws.runUntilDone(); logger.info("done withdrawing test balance"); @@ -360,12 +376,11 @@ export async function runIntegrationTest( const refundAmount = Amounts.parseOrThrow(`${currency}:6`); const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`); - await withdrawTestBalance( - ws, - Amounts.stringify(withdrawAmountTwo), - args.bankBaseUrl, - args.exchangeBaseUrl, - ); + await withdrawTestBalance(ws, { + amount: Amounts.stringify(withdrawAmountTwo), + bankBaseUrl: args.bankBaseUrl, + exchangeBaseUrl: args.exchangeBaseUrl, + }); // Wait until the withdraw is done await ws.runUntilDone(); @@ -410,7 +425,10 @@ export async function runIntegrationTest( logger.trace("integration test: all done!"); } -export async function testPay(ws: InternalWalletState, args: TestPayArgs) { +export async function testPay( + ws: InternalWalletState, + args: TestPayArgs, +): Promise { logger.trace("creating order"); const merchant = { authToken: args.merchantAuthToken, @@ -429,12 +447,28 @@ export async function testPay(ws: InternalWalletState, args: TestPayArgs) { if (!talerPayUri) { console.error("fatal: no taler pay URI received from backend"); process.exit(1); - return; } logger.trace("taler pay URI:", talerPayUri); const result = await preparePayForUri(ws, talerPayUri); if (result.status !== PreparePayResultType.PaymentPossible) { throw Error(`unexpected prepare pay status: ${result.status}`); } - await confirmPay(ws, result.proposalId, undefined); + const r = await confirmPay( + ws, + result.proposalId, + undefined, + args.forcedCoinSel, + ); + if (r.type != ConfirmPayResultType.Done) { + throw Error("payment not done"); + } + const purchase = await ws.db + .mktx((x) => ({ purchases: x.purchases })) + .runReadOnly(async (tx) => { + return tx.purchases.get(result.proposalId); + }); + checkLogicInvariant(!!purchase); + return { + payCoinSelection: purchase.payCoinSelection, + }; } diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts index ca7b76eb5..55c007bbc 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts @@ -31,6 +31,7 @@ function a(x: string): AmountJson { function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo { return { + value: a(current), availableAmount: a(current), coinPub: "foobar", denomPub: { @@ -45,6 +46,7 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo { function fakeAciWithAgeRestriction(current: string, feeDeposit: string): AvailableCoinInfo { return { + value: a(current), availableAmount: a(current), coinPub: "foobar", denomPub: { diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index 080a5049d..b3439067e 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -29,42 +29,14 @@ import { AmountJson, Amounts, DenominationPubKey, + ForcedCoinSel, Logger, + PayCoinSelection, } from "@gnu-taler/taler-util"; +import { checkLogicInvariant } from "./invariants.js"; const logger = new Logger("coinSelection.ts"); -/** - * Result of selecting coins, contains the exchange, and selected - * coins with their denomination. - */ -export interface PayCoinSelection { - /** - * Amount requested by the merchant. - */ - paymentAmount: AmountJson; - - /** - * Public keys of the coins that were selected. - */ - coinPubs: string[]; - - /** - * Amount that each coin contributes. - */ - coinContributions: AmountJson[]; - - /** - * How much of the wire fees is the customer paying? - */ - customerWireFees: AmountJson; - - /** - * How much of the deposit fees is the customer paying? - */ - customerDepositFees: AmountJson; -} - /** * Structure to describe a coin that is available to be * used in a payment. @@ -82,6 +54,11 @@ export interface AvailableCoinInfo { */ denomPub: DenominationPubKey; + /** + * Full value of the coin. + */ + value: AmountJson; + /** * Amount still remaining (typically the full amount, * as coins are always refreshed after use.) @@ -356,3 +333,102 @@ export function selectPayCoins( } return undefined; } + +export function selectForcedPayCoins( + forcedCoinSel: ForcedCoinSel, + req: SelectPayCoinRequest, +): PayCoinSelection | undefined { + const { + candidates, + contractTermsAmount, + depositFeeLimit, + wireFeeLimit, + wireFeeAmortization, + } = req; + + if (candidates.candidateCoins.length === 0) { + return undefined; + } + const coinPubs: string[] = []; + const coinContributions: AmountJson[] = []; + const currency = contractTermsAmount.currency; + + let tally: CoinSelectionTally = { + amountPayRemaining: contractTermsAmount, + amountWireFeeLimitRemaining: wireFeeLimit, + amountDepositFeeLimitRemaining: depositFeeLimit, + customerDepositFees: Amounts.getZero(currency), + customerWireFees: Amounts.getZero(currency), + wireFeeCoveredForExchange: new Set(), + }; + + // Not supported by forced coin selection + checkLogicInvariant(!req.prevPayCoins); + + // Sort by available amount (descending), deposit fee (ascending) and + // denomPub (ascending) if deposit fee is the same + // (to guarantee deterministic results) + const candidateCoins = [...candidates.candidateCoins].sort( + (o1, o2) => + -Amounts.cmp(o1.availableAmount, o2.availableAmount) || + Amounts.cmp(o1.feeDeposit, o2.feeDeposit) || + DenominationPubKey.cmp(o1.denomPub, o2.denomPub), + ); + + // FIXME: Here, we should select coins in a smarter way. + // Instead of always spending the next-largest coin, + // we should try to find the smallest coin that covers the + // amount. + + // Set of spent coin indices from candidate coins + const spentSet: Set = new Set(); + + for (const forcedCoin of forcedCoinSel.coins) { + let aci: AvailableCoinInfo | undefined = undefined; + for (let i = 0; i < candidateCoins.length; i++) { + if (spentSet.has(i)) { + continue; + } + if ( + Amounts.cmp(forcedCoin.value, candidateCoins[i].availableAmount) != 0 + ) { + continue; + } + spentSet.add(i); + aci = candidateCoins[i]; + break; + } + + if (!aci) { + throw Error("can't find coin for forced coin selection"); + } + + tally = tallyFees( + tally, + candidates.wireFeesPerExchange, + wireFeeAmortization, + aci.exchangeBaseUrl, + aci.feeDeposit, + ); + + let coinSpend = Amounts.parseOrThrow(forcedCoin.contribution); + + tally.amountPayRemaining = Amounts.sub( + tally.amountPayRemaining, + coinSpend, + ).amount; + coinPubs.push(aci.coinPub); + coinContributions.push(coinSpend); + } + + if (Amounts.isZero(tally.amountPayRemaining)) { + return { + paymentAmount: contractTermsAmount, + coinContributions, + coinPubs, + customerDepositFees: tally.customerDepositFees, + customerWireFees: tally.customerWireFees, + }; + } + return undefined; +} diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 2fe18cb2c..13a05b385 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -37,7 +37,7 @@ export interface RetryPolicy { const defaultRetryPolicy: RetryPolicy = { backoffBase: 1.5, - backoffDelta: Duration.fromSpec({ seconds: 30 }), + backoffDelta: Duration.fromSpec({ seconds: 1 }), maxTimeout: Duration.fromSpec({ minutes: 2 }), }; diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 0555b0ced..9acfbf103 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -57,6 +57,7 @@ import { SetCoinSuspendedRequest, SetWalletDeviceIdRequest, TestPayArgs, + TestPayResult, TrackDepositGroupRequest, TrackDepositGroupResponse, TransactionsRequest, @@ -270,7 +271,7 @@ export type WalletOperations = { }; [WalletApiOperation.TestPay]: { request: TestPayArgs; - response: {}; + response: TestPayResult; }; [WalletApiOperation.ExportDb]: { request: {}; @@ -279,12 +280,12 @@ export type WalletOperations = { }; export type RequestType< - Op extends WalletApiOperation & keyof WalletOperations - > = WalletOperations[Op] extends { request: infer T } ? T : never; + Op extends WalletApiOperation & keyof WalletOperations, +> = WalletOperations[Op] extends { request: infer T } ? T : never; export type ResponseType< - Op extends WalletApiOperation & keyof WalletOperations - > = WalletOperations[Op] extends { response: infer T } ? T : never; + Op extends WalletApiOperation & keyof WalletOperations, +> = WalletOperations[Op] extends { response: infer T } ? T : never; export interface WalletCoreApiClient { call( diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index a0eaca2e9..c7b94138e 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -23,7 +23,9 @@ * Imports. */ import { - AbsoluteTime, AcceptManualWithdrawalResult, AmountJson, + AbsoluteTime, + AcceptManualWithdrawalResult, + AmountJson, Amounts, BalancesResponse, codecForAbortPayWithRefundRequest, @@ -48,7 +50,9 @@ import { codecForIntegrationTestArgs, codecForListKnownBankAccounts, codecForPrepareDepositRequest, - codecForPreparePayRequest, codecForPrepareRefundRequest, codecForPrepareTipRequest, + codecForPreparePayRequest, + codecForPrepareRefundRequest, + codecForPrepareTipRequest, codecForRetryTransactionRequest, codecForSetCoinSuspendedRequest, codecForSetWalletDeviceIdRequest, @@ -58,7 +62,9 @@ import { codecForWithdrawFakebankRequest, codecForWithdrawTestBalance, CoinDumpJson, - CoreApiResponse, Duration, durationFromSpec, + CoreApiResponse, + Duration, + durationFromSpec, durationMin, ExchangeListItem, ExchangesListRespose, @@ -71,13 +77,14 @@ import { parsePaytoUri, PaytoUri, RefreshReason, - TalerErrorCode, URL, - WalletNotification + TalerErrorCode, + URL, + WalletNotification, } from "@gnu-taler/taler-util"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { CryptoDispatcher, - CryptoWorkerFactory + CryptoWorkerFactory, } from "./crypto/workers/cryptoDispatcher.js"; import { AuditorTrustRecord, @@ -85,7 +92,7 @@ import { exportDb, importDb, ReserveRecordStatus, - WalletStoresV1 + WalletStoresV1, } from "./db.js"; import { getErrorDetailFromException, TalerError } from "./errors.js"; import { @@ -96,7 +103,7 @@ import { MerchantOperations, NotificationListener, RecoupOperations, - ReserveOperations + ReserveOperations, } from "./internal-wallet-state.js"; import { exportBackup } from "./operations/backup/export.js"; import { @@ -109,7 +116,7 @@ import { loadBackupRecovery, processBackupForProvider, removeBackupProvider, - runBackupCycle + runBackupCycle, } from "./operations/backup/index.js"; import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalances } from "./operations/balance.js"; @@ -118,7 +125,7 @@ import { getFeeForDeposit, prepareDepositGroup, processDepositGroup, - trackDepositGroup + trackDepositGroup, } from "./operations/deposits.js"; import { acceptExchangeTermsOfService, @@ -127,66 +134,66 @@ import { getExchangeRequestTimeout, getExchangeTrust, updateExchangeFromUrl, - updateExchangeTermsOfService + updateExchangeTermsOfService, } from "./operations/exchanges.js"; import { getMerchantInfo } from "./operations/merchants.js"; import { confirmPay, preparePayForUri, processDownloadProposal, - processPurchasePay + processPurchasePay, } from "./operations/pay.js"; import { getPendingOperations } from "./operations/pending.js"; import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js"; import { autoRefresh, createRefreshGroup, - processRefreshGroup + processRefreshGroup, } from "./operations/refresh.js"; import { abortFailedPayWithRefund, applyRefund, applyRefundFromPurchaseId, prepareRefund, - processPurchaseQueryRefund + processPurchaseQueryRefund, } from "./operations/refund.js"; import { createReserve, createTalerWithdrawReserve, getFundingPaytoUris, - processReserve + processReserve, } from "./operations/reserves.js"; import { runIntegrationTest, testPay, - withdrawTestBalance + withdrawTestBalance, } from "./operations/testing.js"; import { acceptTip, prepareTip, processTip } from "./operations/tip.js"; import { deleteTransaction, getTransactions, - retryTransaction + retryTransaction, } from "./operations/transactions.js"; import { getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, - processWithdrawGroup + processWithdrawGroup, } from "./operations/withdraw.js"; import { PendingOperationsResponse, PendingTaskInfo, - PendingTaskType + PendingTaskType, } from "./pending-types.js"; import { assertUnreachable } from "./util/assertUnreachable.js"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js"; import { HttpRequestLibrary, - readSuccessResponseJsonOrThrow + readSuccessResponseJsonOrThrow, } from "./util/http.js"; import { AsyncCondition, OpenedPromise, - openPromise + openPromise, } from "./util/promiseUtils.js"; import { DbAccess, GetReadWriteAccess } from "./util/query.js"; import { TimerAPI, TimerGroup } from "./util/timer.js"; @@ -355,7 +362,6 @@ async function runTaskLoop( if (p.givesLifeness) { numGivingLiveness++; } - } if (opts.stopWhenDone && numGivingLiveness === 0 && iteration !== 0) { @@ -459,13 +465,12 @@ async function acceptManualWithdrawal( exchangeBaseUrl: string, amount: AmountJson, restrictAge?: number, - ): Promise { try { const resp = await createReserve(ws, { amount, exchange: exchangeBaseUrl, - restrictAge + restrictAge, }); const exchangePaytoUris = await ws.db .mktx((x) => ({ @@ -688,7 +693,7 @@ async function dumpCoins(ws: InternalWalletState): Promise { c.denomPubHash, ); if (!denomInfo) { - console.error("no denomination found for coin") + console.error("no denomination found for coin"); continue; } coinsJson.coins.push({ @@ -749,22 +754,16 @@ async function dispatchRequestInternal( return {}; } case "withdrawTestkudos": { - await withdrawTestBalance( - ws, - "TESTKUDOS:10", - "https://bank.test.taler.net/", - "https://exchange.test.taler.net/", - ); + await withdrawTestBalance(ws, { + amount: "TESTKUDOS:10", + bankBaseUrl: "https://bank.test.taler.net/", + exchangeBaseUrl: "https://exchange.test.taler.net/", + }); return {}; } case "withdrawTestBalance": { const req = codecForWithdrawTestBalance().decode(payload); - await withdrawTestBalance( - ws, - req.amount, - req.bankBaseUrl, - req.exchangeBaseUrl, - ); + await withdrawTestBalance(ws, req); return {}; } case "runIntegrationTest": { @@ -774,8 +773,7 @@ async function dispatchRequestInternal( } case "testPay": { const req = codecForTestPayArgs().decode(payload); - await testPay(ws, req); - return {}; + return await testPay(ws, req); } case "getTransactions": { const req = codecForTransactionsRequest().decode(payload); @@ -813,7 +811,7 @@ async function dispatchRequestInternal( ws, req.exchangeBaseUrl, Amounts.parseOrThrow(req.amount), - req.restrictAge + req.restrictAge, ); return res; } -- cgit v1.2.3