summaryrefslogtreecommitdiff
path: root/packages/taler-util/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src')
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts28
-rw-r--r--packages/taler-util/src/http-client/types.ts18
-rw-r--r--packages/taler-util/src/http-client/utils.ts26
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);
+ }
+}