From 859d5c56c7a5b26e741254d6d1e9c5731a787ae1 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 8 Apr 2024 22:29:51 +0200 Subject: wallet-core: support cancellation for getWithdrawalDetailsForAmount --- packages/taler-util/src/errors.ts | 8 ++++ packages/taler-util/src/taler-error-codes.ts | 8 ++++ packages/taler-util/src/wallet-types.ts | 13 ++++++ packages/taler-wallet-core/src/wallet.ts | 35 +++++----------- packages/taler-wallet-core/src/withdraw.ts | 60 ++++++++++++++++++++++++++-- 5 files changed, 96 insertions(+), 28 deletions(-) (limited to 'packages') diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts index 11f01a3fe..4dea7e1b6 100644 --- a/packages/taler-util/src/errors.ts +++ b/packages/taler-util/src/errors.ts @@ -25,6 +25,7 @@ */ import { AbsoluteTime, + CancellationToken, PaymentInsufficientBalanceDetails, TalerErrorCode, TalerErrorDetail, @@ -285,6 +286,13 @@ export function getErrorDetailFromException(e: any): TalerErrorDetail { if (e instanceof TalerError) { return e.errorDetail; } + if (e instanceof CancellationToken.CancellationError) { + const err = makeErrorDetail( + TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED, + {}, + ); + return err; + } if (e instanceof Error) { const err = makeErrorDetail( TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts index addb1b047..c3c008a1c 100644 --- a/packages/taler-util/src/taler-error-codes.ts +++ b/packages/taler-util/src/taler-error-codes.ts @@ -3944,6 +3944,14 @@ export enum TalerErrorCode { WALLET_TALER_URI_MALFORMED = 7035, + /** + * A wallet-core request was cancelled and thus can't provide a response. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CORE_REQUEST_CANCELLED = 7036, + + /** * We encountered a timeout with our payment backend. * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index 34d91d3d6..fa0f385b7 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -1857,6 +1857,19 @@ export interface GetWithdrawalDetailsForAmountRequest { exchangeBaseUrl: string; amount: AmountString; restrictAge?: number; + + /** + * ID provided by the client to cancel the request. + * + * If the same request is made again with the same clientCancellationId, + * all previous requests are cancelled. + * + * The cancelled request will receive an error response with + * an error code that indicates the cancellation. + * + * The cancellation is best-effort, responses might still arrive. + */ + clientCancellationId?: string; } export interface AcceptBankIntegratedWithdrawalRequest { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 223272745..d8361c6e4 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -71,7 +71,6 @@ import { WalletCoreVersion, WalletNotification, WalletRunConfig, - WithdrawalDetailsForAmount, checkDbInvariant, codecForAbortTransaction, codecForAcceptBankIntegratedWithdrawalRequest, @@ -291,7 +290,7 @@ import { import { acceptWithdrawalFromUri, createManualWithdrawal, - getExchangeWithdrawalInfo, + getWithdrawalDetailsForAmount, getWithdrawalDetailsForUri, } from "./withdraw.js"; @@ -668,6 +667,7 @@ export interface PendingOperationsResponse { */ async function dispatchRequestInternal( wex: WalletExecutionContext, + cts: CancellationToken.Source, operation: WalletApiOperation, payload: unknown, ): Promise> { @@ -893,27 +893,7 @@ async function dispatchRequestInternal( case WalletApiOperation.GetWithdrawalDetailsForAmount: { const req = codecForGetWithdrawalDetailsForAmountRequest().decode(payload); - const wi = await getExchangeWithdrawalInfo( - wex, - req.exchangeBaseUrl, - Amounts.parseOrThrow(req.amount), - req.restrictAge, - ); - let numCoins = 0; - for (const x of wi.selectedDenoms.selectedDenoms) { - numCoins += x.count; - } - const resp: WithdrawalDetailsForAmount = { - amountRaw: req.amount, - amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue), - paytoUris: wi.exchangePaytoUris, - tosAccepted: wi.termsOfServiceAccepted, - ageRestrictionOptions: wi.ageRestrictionOptions, - withdrawalAccountsList: wi.exchangeCreditAccountDetails, - numCoins, - // FIXME: Once we have proper scope info support, return correct info here. - scopeInfo: wi.scopeInfo, - }; + const resp = await getWithdrawalDetailsForAmount(wex, cts, req); return resp; } case WalletApiOperation.GetBalances: { @@ -1522,6 +1502,8 @@ async function handleCoreApiRequest( let wex: WalletExecutionContext; let oc: ObservabilityContext; + const cts = CancellationToken.create(); + if (ws.initCalled && ws.config.testing.emitObservabilityEvents) { oc = { observe(evt) { @@ -1534,12 +1516,12 @@ async function handleCoreApiRequest( }, }; - wex = getObservedWalletExecutionContext(ws, CancellationToken.CONTINUE, oc); + wex = getObservedWalletExecutionContext(ws, cts.token, oc); } else { oc = { observe(evt) {}, }; - wex = getNormalWalletExecutionContext(ws, CancellationToken.CONTINUE, oc); + wex = getNormalWalletExecutionContext(ws, cts.token, oc); } try { @@ -1550,6 +1532,7 @@ async function handleCoreApiRequest( }); const result = await dispatchRequestInternal( wex, + cts, operation as any, payload, ); @@ -1772,6 +1755,8 @@ export class InternalWalletState { devExperimentState: DevExperimentState = {}; + clientCancellationMap: Map = new Map(); + initWithConfig(newConfig: WalletRunConfig): void { this._config = newConfig; diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 960ffa525..68ff9d494 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -49,6 +49,7 @@ import { ExchangeWithdrawResponse, ExchangeWithdrawalDetails, ForcedDenomSel, + GetWithdrawalDetailsForAmountRequest, HttpStatusCode, LibtoolVersion, Logger, @@ -69,6 +70,7 @@ import { UnblindedSignature, WalletNotification, WithdrawUriInfoResponse, + WithdrawalDetailsForAmount, WithdrawalExchangeAccountDetails, WithdrawalType, addPaytoQueryParams, @@ -1273,7 +1275,6 @@ export async function updateWithdrawalDenoms( wex: WalletExecutionContext, exchangeBaseUrl: string, ): Promise { - logger.trace( `updating denominations used for withdrawal for ${exchangeBaseUrl}`, ); @@ -1931,7 +1932,9 @@ export async function getExchangeWithdrawalInfo( ageRestricted: number | undefined, ): Promise { logger.trace("updating exchange"); - const exchange = await fetchFreshExchange(wex, exchangeBaseUrl); + const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, { + cancellationToken: wex.cancellationToken, + }); if (exchange.currency != instructedAmount.currency) { // Specifying the amount in the conversion input currency is not yet supported. @@ -1947,7 +1950,7 @@ export async function getExchangeWithdrawalInfo( exchange, instructedAmount, }, - CancellationToken.CONTINUE, + wex.cancellationToken, ); logger.trace("updating withdrawal denoms"); @@ -3152,3 +3155,54 @@ async function internalWaitWithdrawalFinal( flag.reset(); } } + +export async function getWithdrawalDetailsForAmount( + wex: WalletExecutionContext, + cts: CancellationToken.Source, + req: GetWithdrawalDetailsForAmountRequest, +): Promise { + const clientCancelKey = req.clientCancellationId + ? `ccid:getWithdrawalDetailsForAmount:${req.clientCancellationId}` + : undefined; + if (clientCancelKey) { + const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey); + if (prevCts) { + prevCts.cancel(); + } + wex.ws.clientCancellationMap.set(clientCancelKey, cts); + } + try { + return internalGetWithdrawalDetailsForAmount(wex, req); + } finally { + if (clientCancelKey && !cts.token.isCancelled) { + wex.ws.clientCancellationMap.delete(clientCancelKey); + } + } +} + +async function internalGetWithdrawalDetailsForAmount( + wex: WalletExecutionContext, + req: GetWithdrawalDetailsForAmountRequest, +): Promise { + const wi = await getExchangeWithdrawalInfo( + wex, + req.exchangeBaseUrl, + Amounts.parseOrThrow(req.amount), + req.restrictAge, + ); + let numCoins = 0; + for (const x of wi.selectedDenoms.selectedDenoms) { + numCoins += x.count; + } + const resp: WithdrawalDetailsForAmount = { + amountRaw: req.amount, + amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue), + paytoUris: wi.exchangePaytoUris, + tosAccepted: wi.termsOfServiceAccepted, + ageRestrictionOptions: wi.ageRestrictionOptions, + withdrawalAccountsList: wi.exchangeCreditAccountDetails, + numCoins, + scopeInfo: wi.scopeInfo, + }; + return resp; +} -- cgit v1.2.3