aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-09-19 12:13:31 +0200
committerFlorian Dold <florian@dold.me>2022-09-19 12:13:31 +0200
commitfd752f3171a76129d2f615535b90c6bebb88d842 (patch)
tree93b49a6e5636cd7eeac2655ec0905aeb0b5b241c
parent548cecca212bb40d56a868736d998c484d721f65 (diff)
wallet-core: hide transient pay errors
-rw-r--r--packages/taler-wallet-core/src/errors.ts23
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts62
-rw-r--r--packages/taler-wallet-core/src/util/retries.ts3
3 files changed, 76 insertions, 12 deletions
diff --git a/packages/taler-wallet-core/src/errors.ts b/packages/taler-wallet-core/src/errors.ts
index d56e936c0..62bde667d 100644
--- a/packages/taler-wallet-core/src/errors.ts
+++ b/packages/taler-wallet-core/src/errors.ts
@@ -70,6 +70,9 @@ export interface DetailsMap {
[TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {};
[TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {};
[TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {};
+ [TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
+ requestError: TalerErrorDetail;
+ };
}
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
@@ -79,7 +82,9 @@ export function makeErrorDetail<C extends TalerErrorCode>(
detail: ErrBody<C>,
hint?: string,
): TalerErrorDetail {
- // FIXME: include default hint?
+ if (!hint && !(detail as any).hint) {
+ hint = getDefaultHint(code);
+ }
return { code, hint, ...detail };
}
@@ -99,6 +104,15 @@ export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
return `Error (${ed.code}/${errName})`;
}
+function getDefaultHint(code: number): string {
+ const errName = TalerErrorCode[code];
+ if (errName) {
+ return `Error (${errName})`;
+ } else {
+ return `Error (<unknown>)`;
+ }
+}
+
export class TalerError<T = any> extends Error {
errorDetail: TalerErrorDetail & T;
private constructor(d: TalerErrorDetail & T) {
@@ -113,12 +127,7 @@ export class TalerError<T = any> extends Error {
hint?: string,
): TalerError {
if (!hint) {
- const errName = TalerErrorCode[code];
- if (errName) {
- hint = `Error (${errName})`;
- } else {
- hint = `Error (<unknown>)`;
- }
+ hint = getDefaultHint(code);
}
return new TalerError<unknown>({ code, hint, ...detail });
}
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index a498ab28d..fb22d0fad 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -83,6 +83,7 @@ import {
EXCHANGE_COINS_LOCK,
InternalWalletState,
} from "../internal-wallet-state.js";
+import { PendingTaskType } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import {
CoinSelectionTally,
@@ -105,7 +106,11 @@ import {
RetryTags,
scheduleRetry,
} from "../util/retries.js";
-import { spendCoins } from "../wallet.js";
+import {
+ spendCoins,
+ storeOperationError,
+ storeOperationPending,
+} from "../wallet.js";
import { getExchangeDetails } from "./exchanges.js";
import { getTotalRefreshCost } from "./refresh.js";
import { makeEventId } from "./transactions.js";
@@ -1519,10 +1524,43 @@ export async function runPayForConfirmPay(
transactionId: makeEventId(TransactionType.Payment, proposalId),
};
}
- case OperationAttemptResultType.Error:
- // FIXME: allocate error code!
- throw Error("payment failed");
+ case OperationAttemptResultType.Error: {
+ // We hide transient errors from the caller.
+ const opRetry = await ws.db
+ .mktx((x) => [x.operationRetries])
+ .runReadOnly(async (tx) =>
+ tx.operationRetries.get(RetryTags.byPaymentProposalId(proposalId)),
+ );
+ const maxRetry = 3;
+ const numRetry = opRetry?.retryInfo.retryCounter ?? 0;
+ if (
+ res.errorDetail.code ===
+ TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR &&
+ numRetry < maxRetry
+ ) {
+ // Pretend the operation is pending instead of reporting
+ // an error, but only up to maxRetry attempts.
+ await storeOperationPending(
+ ws,
+ RetryTags.byPaymentProposalId(proposalId),
+ );
+ return {
+ type: ConfirmPayResultType.Pending,
+ lastError: opRetry?.lastError,
+ transactionId: makeEventId(TransactionType.Payment, proposalId),
+ };
+ } else {
+ // FIXME: allocate error code!
+ await storeOperationError(
+ ws,
+ RetryTags.byPaymentProposalId(proposalId),
+ res.errorDetail,
+ );
+ throw Error("payment failed");
+ }
+ }
case OperationAttemptResultType.Pending:
+ await storeOperationPending(ws, `${PendingTaskType.Pay}:${proposalId}`);
return {
type: ConfirmPayResultType.Pending,
transactionId: makeEventId(TransactionType.Payment, proposalId),
@@ -1536,7 +1574,7 @@ export async function runPayForConfirmPay(
}
/**
- * Add a contract to the wallet and sign coins, and send them.
+ * Confirm payment for a proposal previously claimed by the wallet.
*/
export async function confirmPay(
ws: InternalWalletState,
@@ -1698,6 +1736,20 @@ export async function processPurchasePay(
);
logger.trace(`got resp ${JSON.stringify(resp)}`);
+
+ if (resp.status >= 500 && resp.status <= 599) {
+ const errDetails = await readUnexpectedResponseDetails(resp);
+ return {
+ type: OperationAttemptResultType.Error,
+ errorDetail: makeErrorDetail(
+ TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR,
+ {
+ requestError: errDetails,
+ },
+ ),
+ };
+ }
+
if (resp.status === HttpStatusCode.BadRequest) {
const errDetails = await readUnexpectedResponseDetails(resp);
logger.warn("unexpected 400 response for /pay");
diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts
index b13e9a27b..cef9e072c 100644
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ b/packages/taler-wallet-core/src/util/retries.ts
@@ -205,6 +205,9 @@ export namespace RetryTags {
export function forBackup(backupRecord: BackupProviderRecord): string {
return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`;
}
+ export function byPaymentProposalId(proposalId: string): string {
+ return `${PendingTaskType.Pay}:${proposalId}`;
+ }
}
export async function scheduleRetryInTx(