diff options
author | Sebastian <sebasjm@gmail.com> | 2024-04-12 15:50:03 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-04-12 15:50:03 -0300 |
commit | 2cfd76e76a837be511c668fa1f22564cb86c7990 (patch) | |
tree | 7d3f08f182b9529f2188ec1ffa0d5f7498e24865 | |
parent | 9b0aef33399afa664646715eb4286e3bc0e38fdc (diff) | |
download | wallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.tar.gz wallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.tar.bz2 wallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.zip |
fix #8093
6 files changed, 91 insertions, 14 deletions
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx index 41956b84b..a3bb091c1 100644 --- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -24,28 +24,30 @@ import { HttpStatusCode, PaytoString, PaytoUri, + TalerCorebankApi, TalerErrorCode, TranslatedString, assertUnreachable, buildPayto, parsePaytoUri, - stringifyPaytoUri, + stringifyPaytoUri } from "@gnu-taler/taler-util"; import { InternationalizationAPI, LocalNotificationBanner, + RouteDefinition, ShowInputErrorLabel, notifyInfo, + useBankCoreApiContext, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; +import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js"; import { useBankState } from "../hooks/bank-state.js"; import { useSessionState } from "../hooks/session.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js"; interface Props { @@ -182,12 +184,17 @@ export function PaytoWireTransferForm({ const puri = payto_uri; const sAmount = sendingAmount; - await handleError(async () => { - const request = { + await handleError(async function createTransactionHandleError() { + const request: TalerCorebankApi.CreateTransactionRequest = { payto_uri: puri, amount: sAmount, }; - const resp = await api.createTransaction(credentials, request); + const check = IdempotencyRetry.tryFiveTimes(); + const resp = await api.createTransaction( + credentials, + request, + check, + ); mutate(() => true); if (resp.type === "fail") { switch (resp.case) { @@ -249,6 +256,15 @@ export function PaytoWireTransferForm({ debug: resp.detail, when: AbsoluteTime.now(), }); + case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: { + return notify({ + type: "error", + title: i18n.str`Tried to create the transaction ${check.maxTries} times with different UID but failed.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + when: AbsoluteTime.now(), + }); + } case HttpStatusCode.Accepted: { updateBankState("currentChallenge", { operation: "create-transaction", diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx index eae5bfb5f..624890468 100644 --- a/packages/bank-ui/src/pages/SolveChallengePage.tsx +++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx @@ -48,6 +48,7 @@ import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { undefinedIfEmpty } from "../utils.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; +import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js"; const TAN_PREFIX = "T-"; const TAN_REGEX = /^([Tt](-)?)?[0-9]*$/; @@ -205,7 +206,7 @@ export function SolveChallengePage({ case "update-password": return await api.updatePassword(creds, ch.request, ch.id); case "create-transaction": - return await api.createTransaction(creds, ch.request, ch.id); + return await api.createTransaction(creds, ch.request, undefined, ch.id); case "confirm-withdrawal": return await api.confirmWithdrawalById(creds, ch.request, ch.id); case "create-cashout": diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts index 59698a68b..be37560cd 100644 --- a/packages/taler-util/src/http-client/bank-core.ts +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -19,6 +19,9 @@ import { HttpStatusCode, LibtoolVersion, LongPollParams, + OperationAlternative, + OperationFail, + OperationOk, TalerErrorCode, codecForChallenge, codecForTalerErrorDetail, @@ -64,6 +67,7 @@ import { } from "./types.js"; import { CacheEvictor, + IdempotencyRetry, addLongPollingParam, addPaginationParams, makeBearerTokenAuthHeader, @@ -493,9 +497,25 @@ export class TalerCoreBankHttpClient { async createTransaction( auth: UserAndToken, body: TalerCorebankApi.CreateTransactionRequest, + idempotencyCheck: IdempotencyRetry | undefined, cid?: string, - ) { + ): Promise< + //manually definition all return types because of recursion + | OperationOk<TalerCorebankApi.CreateTransactionResponse> + | OperationAlternative<HttpStatusCode.Accepted, TalerCorebankApi.Challenge> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.BadRequest> + | OperationFail<HttpStatusCode.Unauthorized> + | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT> + | OperationFail<TalerErrorCode.BANK_ADMIN_CREDITOR> + | OperationFail<TalerErrorCode.BANK_SAME_ACCOUNT> + | OperationFail<TalerErrorCode.BANK_UNKNOWN_CREDITOR> + | OperationFail<TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED> + > { const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl); + if (idempotencyCheck) { + body.request_uid = idempotencyCheck.uid + } const resp = await this.httpLib.fetch(url.href, { method: "POST", headers: { @@ -530,6 +550,12 @@ export class TalerCoreBankHttpClient { return opKnownTalerFailure(details.code, details); case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownTalerFailure(details.code, details); + case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: + if (!idempotencyCheck) { + return opKnownTalerFailure(details.code, details); + } + const nextRetry = idempotencyCheck.next(); + return this.createTransaction(auth, body, nextRetry, cid); default: return opUnknownFailure(resp, details); } diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index c843e075a..35603264a 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -825,7 +825,10 @@ export const codecForTemplateDetails = .property("otp_id", codecOptional(codecForString())) .property("template_contract", codecForTemplateContractDetails()) .property("required_currency", codecOptional(codecForString())) - .property("editable_defaults", codecOptional(codecForTemplateContractDetailsDefaults())) + .property( + "editable_defaults", + codecOptional(codecForTemplateContractDetailsDefaults()), + ) .build("TalerMerchantApi.TemplateDetails"); export const codecForTemplateContractDetails = @@ -853,7 +856,10 @@ export const codecForWalletTemplateDetails = buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>() .property("template_contract", codecForTemplateContractDetails()) .property("required_currency", codecOptional(codecForString())) - .property("editable_defaults", codecOptional(codecForTemplateContractDetailsDefaults())) + .property( + "editable_defaults", + codecOptional(codecForTemplateContractDetailsDefaults()), + ) .build("TalerMerchantApi.WalletTemplateDetails"); export const codecForWebhookSummaryResponse = @@ -2083,6 +2089,12 @@ export namespace TalerCorebankApi { // query string parameter of the 'payto' field. In case it // is given in both places, the paytoUri's takes the precedence. amount?: AmountString; + + // Nonce to make the request idempotent. Requests with the same + // request_uid that differ in any of the other fields + // are rejected. + // @since v4, will become mandatory in the next version. + request_uid?: ShortHashCode; } export interface CreateTransactionResponse { @@ -4636,7 +4648,6 @@ export namespace TalerMerchantApi { // This parameter is optional. // Since protocol **v13**. required_currency?: string; - } export interface TemplateContractDetails { // Human-readable summary for the template. @@ -4699,7 +4710,6 @@ export namespace TalerMerchantApi { // This parameter is optional. // Since protocol **v13**. required_currency?: string; - } export interface TemplateSummaryResponse { diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts index d6623cf00..c579cd852 100644 --- a/packages/taler-util/src/http-client/utils.ts +++ b/packages/taler-util/src/http-client/utils.ts @@ -18,7 +18,7 @@ * Imports. */ import { base64FromArrayBuffer } from "../base64.js"; -import { stringToBytes } from "../taler-crypto.js"; +import { encodeCrock, getRandomBytes, stringToBytes } from "../taler-crypto.js"; import { AccessToken, LongPollParams, PaginationParams } from "./types.js"; /** @@ -90,3 +90,27 @@ export interface CacheEvictor<T> { export const nullEvictor: CacheEvictor<unknown> = { notifySuccess: () => Promise.resolve(), }; + +export class IdempotencyRetry { + public readonly uid: string; + public readonly timesLeft: number; + public readonly maxTries: number; + + private constructor(timesLeft: number, maxTimesLeft: number) { + this.timesLeft = timesLeft; + this.maxTries = maxTimesLeft; + this.uid = encodeCrock(getRandomBytes(32)) + } + + static tryFiveTimes() { + return new IdempotencyRetry(5, 5) + } + + next(): IdempotencyRetry | undefined { + const left = this.timesLeft -1 + if (left <= 0) { + return undefined + } + return new IdempotencyRetry(left, this.maxTries); + } +} diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx index 6666413eb..8b6377fc5 100644 --- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx +++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx @@ -218,7 +218,7 @@ export function BankDetailsByPaytoType({ <i18n.Translate> If you don't already have it in your banking favourites list, - then copy and past this IBAN and the name into the receiver + then copy and paste this IBAN and the name into the receiver fields in your banking app or website </i18n.Translate> </td> |