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 /packages/taler-util | |
parent | 9b0aef33399afa664646715eb4286e3bc0e38fdc (diff) | |
download | wallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.tar.xz |
fix #8093
Diffstat (limited to 'packages/taler-util')
-rw-r--r-- | packages/taler-util/src/http-client/bank-core.ts | 28 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/types.ts | 18 | ||||
-rw-r--r-- | packages/taler-util/src/http-client/utils.ts | 26 |
3 files changed, 66 insertions, 6 deletions
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); + } +} |