summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-04-12 15:50:03 -0300
committerSebastian <sebasjm@gmail.com>2024-04-12 15:50:03 -0300
commit2cfd76e76a837be511c668fa1f22564cb86c7990 (patch)
tree7d3f08f182b9529f2188ec1ffa0d5f7498e24865
parent9b0aef33399afa664646715eb4286e3bc0e38fdc (diff)
downloadwallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.tar.gz
wallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.tar.bz2
wallet-core-2cfd76e76a837be511c668fa1f22564cb86c7990.zip
fix #8093
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx28
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx3
-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
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx2
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({
&nbsp;
<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>