wallet-core: hide transient pay errors
This commit is contained in:
parent
548cecca21
commit
fd752f3171
@ -70,6 +70,9 @@ export interface DetailsMap {
|
|||||||
[TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {};
|
[TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {};
|
||||||
[TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {};
|
[TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {};
|
||||||
[TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {};
|
[TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {};
|
||||||
|
[TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
|
||||||
|
requestError: TalerErrorDetail;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
|
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
|
||||||
@ -79,7 +82,9 @@ export function makeErrorDetail<C extends TalerErrorCode>(
|
|||||||
detail: ErrBody<C>,
|
detail: ErrBody<C>,
|
||||||
hint?: string,
|
hint?: string,
|
||||||
): TalerErrorDetail {
|
): TalerErrorDetail {
|
||||||
// FIXME: include default hint?
|
if (!hint && !(detail as any).hint) {
|
||||||
|
hint = getDefaultHint(code);
|
||||||
|
}
|
||||||
return { code, hint, ...detail };
|
return { code, hint, ...detail };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +104,15 @@ export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
|
|||||||
return `Error (${ed.code}/${errName})`;
|
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 {
|
export class TalerError<T = any> extends Error {
|
||||||
errorDetail: TalerErrorDetail & T;
|
errorDetail: TalerErrorDetail & T;
|
||||||
private constructor(d: TalerErrorDetail & T) {
|
private constructor(d: TalerErrorDetail & T) {
|
||||||
@ -113,12 +127,7 @@ export class TalerError<T = any> extends Error {
|
|||||||
hint?: string,
|
hint?: string,
|
||||||
): TalerError {
|
): TalerError {
|
||||||
if (!hint) {
|
if (!hint) {
|
||||||
const errName = TalerErrorCode[code];
|
hint = getDefaultHint(code);
|
||||||
if (errName) {
|
|
||||||
hint = `Error (${errName})`;
|
|
||||||
} else {
|
|
||||||
hint = `Error (<unknown>)`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return new TalerError<unknown>({ code, hint, ...detail });
|
return new TalerError<unknown>({ code, hint, ...detail });
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,7 @@ import {
|
|||||||
EXCHANGE_COINS_LOCK,
|
EXCHANGE_COINS_LOCK,
|
||||||
InternalWalletState,
|
InternalWalletState,
|
||||||
} from "../internal-wallet-state.js";
|
} from "../internal-wallet-state.js";
|
||||||
|
import { PendingTaskType } from "../pending-types.js";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
import {
|
import {
|
||||||
CoinSelectionTally,
|
CoinSelectionTally,
|
||||||
@ -105,7 +106,11 @@ import {
|
|||||||
RetryTags,
|
RetryTags,
|
||||||
scheduleRetry,
|
scheduleRetry,
|
||||||
} from "../util/retries.js";
|
} from "../util/retries.js";
|
||||||
import { spendCoins } from "../wallet.js";
|
import {
|
||||||
|
spendCoins,
|
||||||
|
storeOperationError,
|
||||||
|
storeOperationPending,
|
||||||
|
} from "../wallet.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { getTotalRefreshCost } from "./refresh.js";
|
import { getTotalRefreshCost } from "./refresh.js";
|
||||||
import { makeEventId } from "./transactions.js";
|
import { makeEventId } from "./transactions.js";
|
||||||
@ -1519,10 +1524,43 @@ export async function runPayForConfirmPay(
|
|||||||
transactionId: makeEventId(TransactionType.Payment, proposalId),
|
transactionId: makeEventId(TransactionType.Payment, proposalId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case OperationAttemptResultType.Error:
|
case OperationAttemptResultType.Error: {
|
||||||
// FIXME: allocate error code!
|
// We hide transient errors from the caller.
|
||||||
throw Error("payment failed");
|
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:
|
case OperationAttemptResultType.Pending:
|
||||||
|
await storeOperationPending(ws, `${PendingTaskType.Pay}:${proposalId}`);
|
||||||
return {
|
return {
|
||||||
type: ConfirmPayResultType.Pending,
|
type: ConfirmPayResultType.Pending,
|
||||||
transactionId: makeEventId(TransactionType.Payment, proposalId),
|
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(
|
export async function confirmPay(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -1698,6 +1736,20 @@ export async function processPurchasePay(
|
|||||||
);
|
);
|
||||||
|
|
||||||
logger.trace(`got resp ${JSON.stringify(resp)}`);
|
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) {
|
if (resp.status === HttpStatusCode.BadRequest) {
|
||||||
const errDetails = await readUnexpectedResponseDetails(resp);
|
const errDetails = await readUnexpectedResponseDetails(resp);
|
||||||
logger.warn("unexpected 400 response for /pay");
|
logger.warn("unexpected 400 response for /pay");
|
||||||
|
@ -205,6 +205,9 @@ export namespace RetryTags {
|
|||||||
export function forBackup(backupRecord: BackupProviderRecord): string {
|
export function forBackup(backupRecord: BackupProviderRecord): string {
|
||||||
return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`;
|
return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`;
|
||||||
}
|
}
|
||||||
|
export function byPaymentProposalId(proposalId: string): string {
|
||||||
|
return `${PendingTaskType.Pay}:${proposalId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function scheduleRetryInTx(
|
export async function scheduleRetryInTx(
|
||||||
|
Loading…
Reference in New Issue
Block a user