taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit a8d66b9b095467f414a156dc12ec4f1d2ec3e1c7
parent 2609c7359356f1e991e41eceb7221c424d53bd1b
Author: Florian Dold <florian@dold.me>
Date:   Mon, 15 Jul 2024 15:45:12 +0200

wallet-core: support for clientCancellationId in more requests

Added for:
- checkPeerPushDebit
- checkPeerPullCredit

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 30++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/common.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 18+++++++++++++++++-
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 13+++++++++++++
Mpackages/taler-wallet-core/src/wallet.ts | 4++--
Mpackages/taler-wallet-core/src/withdraw.ts | 42+++++++-----------------------------------
6 files changed, 119 insertions(+), 38 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -2805,6 +2805,19 @@ export interface CheckPeerPushDebitRequest { * FIXME: Allow specifying the instructed amount type. */ amount: AmountString; + + /** + * 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 const codecForCheckPeerPushDebitRequest = @@ -2812,6 +2825,7 @@ export const codecForCheckPeerPushDebitRequest = buildCodecForObject<CheckPeerPushDebitRequest>() .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl())) .property("amount", codecForAmountString()) + .property("clientCancellationId", codecOptional(codecForString())) .build("CheckPeerPushDebitRequest"); export interface CheckPeerPushDebitResponse { @@ -2944,12 +2958,27 @@ export const codecForAcceptPeerPullPaymentRequest = export interface CheckPeerPullCreditRequest { exchangeBaseUrl?: string; amount: AmountString; + + /** + * 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 const codecForPreparePeerPullPaymentRequest = (): Codec<CheckPeerPullCreditRequest> => buildCodecForObject<CheckPeerPullCreditRequest>() .property("amount", codecForAmountString()) .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl())) + .property("clientCancellationId", codecOptional(codecForString())) .build("CheckPeerPullCreditRequest"); export interface CheckPeerPullCreditResponse { @@ -2963,6 +2992,7 @@ export interface CheckPeerPullCreditResponse { */ numCoins: number; } + export interface InitiatePeerPullCreditRequest { exchangeBaseUrl?: string; partialContractTerms: PeerContractTerms; diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -30,6 +30,7 @@ import { ExchangeTosStatus, ExchangeUpdateStatus, Logger, + ObservabilityEventType, RefreshReason, TalerError, TalerErrorCode, @@ -866,3 +867,52 @@ export function requireExchangeTosAcceptedOrThrow( ); } } + +/** + * Run a request, but cancel the wallet execution context as soon as the client + * submits another request of the same type. + */ +export async function runWithClientCancellation<R, T>( + wex: WalletExecutionContext, + operation: string, + clientCancellationId: string | undefined, + handler: () => Promise<T>, +): Promise<T> { + const clientCancelKey = clientCancellationId + ? `ccid:${operation}:${clientCancellationId}` + : undefined; + const cts = wex.cts; + if (clientCancelKey && cts) { + const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey); + if (prevCts) { + wex.oc.observe({ + type: ObservabilityEventType.Message, + contents: `Cancelling previous key ${clientCancelKey}`, + }); + prevCts.cancel( + `cancelled by subsequent request with same cancellation ID`, + ); + } else { + wex.oc.observe({ + type: ObservabilityEventType.Message, + contents: `No previous key ${clientCancelKey}`, + }); + } + wex.oc.observe({ + type: ObservabilityEventType.Message, + contents: `Setting clientCancelKey ${clientCancelKey} to ${cts}`, + }); + wex.ws.clientCancellationMap.set(clientCancelKey, cts); + } + try { + return await handler(); + } finally { + wex.oc.observe({ + type: ObservabilityEventType.Message, + contents: `Deleting clientCancelKey ${clientCancelKey} to ${cts}`, + }); + if (clientCancelKey && wex.cts && !wex.cts.token.isCancelled) { + wex.ws.clientCancellationMap.delete(clientCancelKey); + } + } +} diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -60,6 +60,7 @@ import { TransactionContext, constructTaskIdentifier, requireExchangeTosAcceptedOrThrow, + runWithClientCancellation, } from "./common.js"; import { KycPendingInfo, @@ -920,7 +921,22 @@ async function processPeerPullCreditKycRequired( /** * Check fees and available exchanges for a peer push payment initiation. */ -export async function checkPeerPullPaymentInitiation( +export async function checkPeerPullCredit( + wex: WalletExecutionContext, + req: CheckPeerPullCreditRequest, +): Promise<CheckPeerPullCreditResponse> { + return runWithClientCancellation( + wex, + "checkPeerPullCredit", + req.clientCancellationId, + async () => internalCheckPeerPullCredit(wex, req), + ); +} + +/** + * Check fees and available exchanges for a peer push payment initiation. + */ +export async function internalCheckPeerPullCredit( wex: WalletExecutionContext, req: CheckPeerPullCreditRequest, ): Promise<CheckPeerPullCreditResponse> { diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -63,6 +63,7 @@ import { TaskRunResultType, TransactionContext, constructTaskIdentifier, + runWithClientCancellation, spendCoins, } from "./common.js"; import { EncryptContractRequest } from "./crypto/cryptoTypes.js"; @@ -350,6 +351,18 @@ export async function checkPeerPushDebit( wex: WalletExecutionContext, req: CheckPeerPushDebitRequest, ): Promise<CheckPeerPushDebitResponse> { + return runWithClientCancellation( + wex, + "checkPeerPushDebit", + req.clientCancellationId, + () => internalCheckPeerPushDebit(wex, req), + ); +} + +async function internalCheckPeerPushDebit( + wex: WalletExecutionContext, + req: CheckPeerPushDebitRequest, +): Promise<CheckPeerPushDebitResponse> { const instructedAmount = Amounts.parseOrThrow(req.amount); logger.trace( `checking peer push debit for ${Amounts.stringify(instructedAmount)}`, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -290,7 +290,7 @@ import { startRefundQueryForUri, } from "./pay-merchant.js"; import { - checkPeerPullPaymentInitiation, + checkPeerPullCredit, initiatePeerPullPayment, } from "./pay-peer-pull-credit.js"; import { @@ -1920,7 +1920,7 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { }, [WalletApiOperation.CheckPeerPullCredit]: { codec: codecForPreparePeerPullPaymentRequest(), - handler: checkPeerPullPaymentInitiation, + handler: checkPeerPullCredit, }, [WalletApiOperation.InitiatePeerPullCredit]: { codec: codecForInitiatePeerPullPaymentRequest(), diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -122,6 +122,7 @@ import { makeCoinAvailable, makeCoinsVisible, requireExchangeTosAcceptedOrThrow, + runWithClientCancellation, } from "./common.js"; import { EddsaKeypair } from "./crypto/cryptoImplementation.js"; import { @@ -3808,41 +3809,12 @@ export async function getWithdrawalDetailsForAmount( wex: WalletExecutionContext, req: GetWithdrawalDetailsForAmountRequest, ): Promise<WithdrawalDetailsForAmount> { - const clientCancelKey = req.clientCancellationId - ? `ccid:getWithdrawalDetailsForAmount:${req.clientCancellationId}` - : undefined; - const cts = wex.cts; - if (clientCancelKey && cts) { - const prevCts = wex.ws.clientCancellationMap.get(clientCancelKey); - if (prevCts) { - wex.oc.observe({ - type: ObservabilityEventType.Message, - contents: `Cancelling previous key ${clientCancelKey}`, - }); - prevCts.cancel(`getting details amount`); - } else { - wex.oc.observe({ - type: ObservabilityEventType.Message, - contents: `No previous key ${clientCancelKey}`, - }); - } - wex.oc.observe({ - type: ObservabilityEventType.Message, - contents: `Setting clientCancelKey ${clientCancelKey} to ${cts}`, - }); - wex.ws.clientCancellationMap.set(clientCancelKey, cts); - } - try { - return await internalGetWithdrawalDetailsForAmount(wex, req); - } finally { - wex.oc.observe({ - type: ObservabilityEventType.Message, - contents: `Deleting clientCancelKey ${clientCancelKey} to ${cts}`, - }); - if (clientCancelKey && wex.cts && !wex.cts.token.isCancelled) { - wex.ws.clientCancellationMap.delete(clientCancelKey); - } - } + return runWithClientCancellation( + wex, + "getWithdrawalDetailsForAmount", + req.clientCancellationId, + async () => internalGetWithdrawalDetailsForAmount(wex, req), + ); } async function internalGetWithdrawalDetailsForAmount(