wallet: improve error handling and error codes

This commit is contained in:
Florian Dold 2022-03-22 21:16:38 +01:00
parent f8d12f7b0d
commit 5d23eb3635
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
41 changed files with 672 additions and 508 deletions

View File

@ -22,7 +22,7 @@
/**
* Imports.
*/
import { TalerErrorDetails } from "./walletTypes.js";
import { TalerErrorDetail } from "./walletTypes.js";
export enum NotificationType {
CoinWithdrawn = "coin-withdrawn",
@ -157,62 +157,62 @@ export interface ExchangeAddedNotification {
export interface ExchangeOperationErrorNotification {
type: NotificationType.ExchangeOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface RefreshOperationErrorNotification {
type: NotificationType.RefreshOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface RefundStatusOperationErrorNotification {
type: NotificationType.RefundStatusOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface RefundApplyOperationErrorNotification {
type: NotificationType.RefundApplyOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface PayOperationErrorNotification {
type: NotificationType.PayOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface ProposalOperationErrorNotification {
type: NotificationType.ProposalOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface TipOperationErrorNotification {
type: NotificationType.TipOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface WithdrawOperationErrorNotification {
type: NotificationType.WithdrawOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface RecoupOperationErrorNotification {
type: NotificationType.RecoupOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface DepositOperationErrorNotification {
type: NotificationType.DepositOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface ReserveOperationErrorNotification {
type: NotificationType.ReserveOperationError;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface ReserveCreatedNotification {

View File

@ -22,6 +22,8 @@
*/
export enum TalerErrorCode {
/**
* Special code to indicate success (no error).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@ -78,6 +80,13 @@ export enum TalerErrorCode {
*/
GENERIC_CONFIGURATION_INVALID = 14,
/**
* The client made a request to a service, but received an error response it does not know how to handle.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_UNEXPECTED_REQUEST_ERROR = 15,
/**
* The HTTP method used is invalid for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
@ -372,6 +381,20 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
/**
* The reserve public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_GENERIC_RESERVE_PUB_MALFORMED = 1019,
/**
* The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_GENERIC_CLOCK_SKEW = 1020,
/**
* The exchange did not find information about the specified transaction in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@ -541,11 +564,25 @@ export enum TalerErrorCode {
EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
/**
* The reserve status was requested using a unknown key.
* The reserve balance, status or history was requested for a reserve which is not known to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_GET_STATUS_UNKNOWN = 1250,
EXCHANGE_RESERVES_STATUS_UNKNOWN = 1250,
/**
* The reserve status was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE = 1251,
/**
* The reserve history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE = 1252,
/**
* The exchange encountered melt fees exceeding the melted coin's contribution.
@ -1394,6 +1431,27 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED = 2170,
/**
* The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING = 2171,
/**
* The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH = 2172,
/**
* The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED = 2173,
/**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@ -2150,6 +2208,13 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_MALFORMED = 7020,
/**
* A pending operation failed, and thus the request can't be completed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
WALLET_PENDING_OPERATION_FAILED = 7021,
/**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@ -2646,4 +2711,5 @@ export enum TalerErrorCode {
* (A value of 0 indicates that the error is generated client-side).
*/
END = 9999,
}

View File

@ -42,7 +42,7 @@ import {
codecForList,
codecForAny,
} from "./codec.js";
import { TalerErrorDetails } from "./walletTypes.js";
import { TalerErrorDetail } from "./walletTypes.js";
export interface TransactionsRequest {
/**
@ -92,7 +92,7 @@ export interface TransactionCommon {
// Amount added or removed from the wallet's balance (including all fees and other costs)
amountEffective: AmountString;
error?: TalerErrorDetails;
error?: TalerErrorDetail;
}
export type Transaction =

View File

@ -60,6 +60,7 @@ import {
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
import { BackupRecovery } from "./backupTypes.js";
import { PaytoUri } from "./payto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
/**
* Response for the create reserve request to the wallet.
@ -136,7 +137,7 @@ export interface ConfirmPayResultDone {
export interface ConfirmPayResultPending {
type: ConfirmPayResultType.Pending;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
@ -455,11 +456,10 @@ export interface WalletDiagnostics {
dbOutdated: boolean;
}
export interface TalerErrorDetails {
code: number;
hint: string;
message: string;
details: unknown;
export interface TalerErrorDetail {
code: TalerErrorCode;
hint?: string;
[x: string]: unknown;
}
/**
@ -850,7 +850,7 @@ export interface CoreApiResponseError {
type: "error";
operation: string;
id: string;
error: TalerErrorDetails;
error: TalerErrorDetail;
}
export interface WithdrawTestBalanceRequest {

View File

@ -49,7 +49,7 @@ import {
HarnessExchangeBankAccount,
NodeHttpLib,
openPromise,
OperationFailedError,
TalerError,
WalletCoreApiClient,
} from "@gnu-taler/taler-wallet-core";
import {
@ -227,19 +227,19 @@ export class GlobalTestState {
this.servers = [];
}
async assertThrowsOperationErrorAsync(
async assertThrowsTalerErrorAsync(
block: () => Promise<void>,
): Promise<OperationFailedError> {
): Promise<TalerError> {
try {
await block();
} catch (e) {
if (e instanceof OperationFailedError) {
if (e instanceof TalerError) {
return e;
}
throw Error(`expected OperationFailedError to be thrown, but got ${e}`);
throw Error(`expected TalerError to be thrown, but got ${e}`);
}
throw Error(
`expected OperationFailedError to be thrown, but block finished without throwing`,
`expected TalerError to be thrown, but block finished without throwing`,
);
}
@ -1904,7 +1904,7 @@ export class WalletCli {
throw new Error("wallet CLI did not return a proper JSON response");
}
if (ar.type === "error") {
throw new OperationFailedError(ar.error);
throw TalerError.fromUncheckedDetail(ar.error);
}
return ar.result;
},

View File

@ -49,14 +49,13 @@ import {
import {
NodeHttpLib,
getDefaultNodeWallet,
OperationFailedAndReportedError,
OperationFailedError,
NodeThreadCryptoWorkerFactory,
CryptoApi,
walletCoreDebugFlags,
WalletApiOperation,
WalletCoreApiClient,
Wallet,
getErrorDetailFromException,
} from "@gnu-taler/taler-wallet-core";
import { lintExchangeDeployment } from "./lint.js";
import { runBench1 } from "./bench1.js";
@ -206,18 +205,12 @@ async function withWallet<T>(
const ret = await f(w);
return ret;
} catch (e) {
if (
e instanceof OperationFailedAndReportedError ||
e instanceof OperationFailedError
) {
console.error("Operation failed: " + e.message);
console.error(
"Error details:",
JSON.stringify(e.operationError, undefined, 2),
);
} else {
console.error("caught unhandled exception (bug?):", e);
}
const ed = getErrorDetailFromException(e);
console.error("Operation failed: " + ed.message);
console.error(
"Error details:",
JSON.stringify(ed.operationError, undefined, 2),
);
process.exit(1);
} finally {
logger.info("operation with wallet finished, stopping");

View File

@ -32,7 +32,6 @@ import {
BankApi,
BankAccessApi,
CreditDebitIndicator,
OperationFailedError,
} from "@gnu-taler/taler-wallet-core";
/**
@ -104,10 +103,10 @@ export async function runBankApiTest(t: GlobalTestState) {
// Make sure that registering twice results in a 409 Conflict
{
const e = await t.assertThrowsAsync(async () => {
const e = await t.assertThrowsTalerErrorAsync(async () => {
await BankApi.registerAccount(bank, "user1", "pw1");
});
t.assertTrue(e.details.httpStatusCode === 409);
t.assertTrue(e.errorDetail.httpStatusCode === 409);
}
let balResp = await BankAccessApi.getAccountBalance(bank, bankUser);

View File

@ -20,25 +20,21 @@
import {
PreparePayResultType,
TalerErrorCode,
TalerErrorDetails,
TransactionType,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
import {
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { makeEventId } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
} from "../harness/helpers.js";
export async function runDenomUnofferedTest(t: GlobalTestState) {
// Set up test environment
const {
wallet,
bank,
exchange,
merchant,
} = await createSimpleTestkudosEnvironment(t);
const { wallet, bank, exchange, merchant } =
await createSimpleTestkudosEnvironment(t);
// Withdraw digital cash into the wallet.
@ -95,19 +91,23 @@ export async function runDenomUnofferedTest(t: GlobalTestState) {
preparePayResult.status === PreparePayResultType.PaymentPossible,
);
const exc = await t.assertThrowsAsync(async () => {
const exc = await t.assertThrowsTalerErrorAsync(async () => {
await wallet.client.call(WalletApiOperation.ConfirmPay, {
proposalId: preparePayResult.proposalId,
});
});
const errorDetails: TalerErrorDetails = exc.operationError;
t.assertTrue(
exc.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
);
// FIXME: We might want a more specific error code here!
t.assertDeepEqual(
errorDetails.code,
exc.errorDetail.innerError.code,
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
);
const merchantErrorCode = (errorDetails.details as any).errorResponse.code;
const merchantErrorCode = (exc.errorDetail.innerError.errorResponse as any)
.code;
t.assertDeepEqual(
merchantErrorCode,
TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,

View File

@ -181,16 +181,22 @@ export async function runExchangeManagementTest(t: GlobalTestState) {
},
});
const err1 = await t.assertThrowsOperationErrorAsync(async () => {
const err1 = await t.assertThrowsTalerErrorAsync(async () => {
await wallet.client.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: faultyExchange.baseUrl,
});
});
// Updating the exchange from the base URL is technically a pending operation
// and it will be retried later.
t.assertTrue(
err1.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
);
// Response is malformed, since it didn't even contain a version code
// in a format the wallet can understand.
t.assertTrue(
err1.operationError.code ===
err1.errorDetail.innerError.code ===
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
);
@ -223,14 +229,18 @@ export async function runExchangeManagementTest(t: GlobalTestState) {
},
});
const err2 = await t.assertThrowsOperationErrorAsync(async () => {
const err2 = await t.assertThrowsTalerErrorAsync(async () => {
await wallet.client.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: faultyExchange.baseUrl,
});
});
t.assertTrue(
err2.operationError.code ===
err2.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
);
t.assertTrue(
err2.errorDetail.innerError.code ===
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
);

View File

@ -123,7 +123,7 @@ export async function runPayAbortTest(t: GlobalTestState) {
},
});
await t.assertThrowsOperationErrorAsync(async () => {
await t.assertThrowsTalerErrorAsync(async () => {
await wallet.client.call(WalletApiOperation.ConfirmPay, {
proposalId: preparePayResult.proposalId,
});

View File

@ -17,8 +17,15 @@
/**
* Imports.
*/
import { GlobalTestState, MerchantPrivateApi, WalletCli } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
import {
GlobalTestState,
MerchantPrivateApi,
WalletCli,
} from "../harness/harness.js";
import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
} from "../harness/helpers.js";
import { PreparePayResultType } from "@gnu-taler/taler-util";
import { TalerErrorCode } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@ -29,12 +36,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runPaymentClaimTest(t: GlobalTestState) {
// Set up test environment
const {
wallet,
bank,
exchange,
merchant,
} = await createSimpleTestkudosEnvironment(t);
const { wallet, bank, exchange, merchant } =
await createSimpleTestkudosEnvironment(t);
const walletTwo = new WalletCli(t, "two");
@ -73,7 +76,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) {
preparePayResult.status === PreparePayResultType.PaymentPossible,
);
t.assertThrowsOperationErrorAsync(async () => {
t.assertThrowsTalerErrorAsync(async () => {
await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
talerPayUri,
});
@ -93,14 +96,19 @@ export async function runPaymentClaimTest(t: GlobalTestState) {
walletTwo.deleteDatabase();
const err = await t.assertThrowsOperationErrorAsync(async () => {
const err = await t.assertThrowsTalerErrorAsync(async () => {
await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
talerPayUri,
});
});
t.assertTrue(
err.operationError.code === TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
err.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
);
t.assertTrue(
err.errorDetail.innerError.code ===
TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
);
await t.shutdown();

View File

@ -32,7 +32,7 @@ import {
ConfirmPayResultType,
PreparePayResultType,
TalerErrorCode,
TalerErrorDetails,
TalerErrorDetail,
URL,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@ -135,11 +135,9 @@ export async function runPaymentTransientTest(t: GlobalTestState) {
}
faultInjected = true;
console.log("injecting pay fault");
const err: TalerErrorDetails = {
const err: TalerErrorDetail = {
code: TalerErrorCode.GENERIC_DB_COMMIT_FAILED,
details: {},
hint: "huh",
message: "something went wrong",
hint: "something went wrong",
};
ctx.responseBody = Buffer.from(JSON.stringify(err));
ctx.statusCode = 500;

View File

@ -26,9 +26,9 @@ import {
findDenomOrThrow,
generateReserveKeypair,
NodeHttpLib,
OperationFailedError,
refreshCoin,
SynchronousCryptoWorkerFactory,
TalerError,
topupReserveWithDemobank,
withdrawCoin,
} from "@gnu-taler/taler-wallet-core";
@ -95,9 +95,9 @@ export async function runWalletDblessTest(t: GlobalTestState) {
newDenoms: refreshDenoms,
});
} catch (e) {
if (e instanceof OperationFailedError) {
if (e instanceof TalerError) {
console.log(e);
console.log(j2s(e.operationError));
console.log(j2s(e.errorDetail));
} else {
console.log(e);
}

View File

@ -63,7 +63,7 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
//
// WHY ?!
//
const e = await t.assertThrowsOperationErrorAsync(async () => {
const e = await t.assertThrowsTalerErrorAsync(async () => {
await wallet.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
@ -73,7 +73,7 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
);
});
t.assertDeepEqual(
e.operationError.code,
e.errorDetail.code,
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
);

View File

@ -31,7 +31,9 @@ import {
getRandomBytes,
j2s,
Logger,
TalerErrorCode,
} from "@gnu-taler/taler-util";
import { TalerError } from "./errors.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrErrorCode,
@ -104,15 +106,20 @@ export namespace BankApi {
let paytoUri = `payto://x-taler-bank/localhost/${username}`;
if (resp.status !== 200 && resp.status !== 202) {
logger.error(`${j2s(await resp.json())}`);
throw new Error();
}
const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny());
// LibEuFin demobank returns payto URI in response
if (respJson.paytoUri) {
paytoUri = respJson.paytoUri;
throw TalerError.fromDetail(
TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
{
httpStatusCode: resp.status,
},
);
}
try {
// Pybank has no body, thus this might throw.
const respJson = await resp.json();
// LibEuFin demobank returns payto URI in response
if (respJson.paytoUri) {
paytoUri = respJson.paytoUri;
}
} catch (e) {}
return {
password,

View File

@ -35,7 +35,7 @@ import {
MerchantInfo,
Product,
RefreshReason,
TalerErrorDetails,
TalerErrorDetail,
UnblindedSignature,
CoinEnvelope,
TalerProtocolTimestamp,
@ -229,7 +229,7 @@ export interface ReserveRecord {
* Last error that happened in a reserve operation
* (either talking to the bank or the exchange).
*/
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
/**
@ -545,7 +545,7 @@ export interface ExchangeRecord {
* Last error (if any) for fetching updated information about the
* exchange.
*/
lastError?: TalerErrorDetails;
lastError?: TalerErrorDetail;
/**
* Retry status for fetching updated information about the exchange.
@ -580,7 +580,7 @@ export interface PlanchetRecord {
withdrawalDone: boolean;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
/**
* Public key of the reserve that this planchet
@ -820,14 +820,14 @@ export interface ProposalRecord {
*/
retryInfo?: RetryInfo;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
/**
* Status of a tip we got from a merchant.
*/
export interface TipRecord {
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
@ -922,9 +922,9 @@ export interface RefreshGroupRecord {
*/
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetails };
lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetail };
/**
* Unique, randomly generated identifier for this group of
@ -1256,7 +1256,7 @@ export interface PurchaseRecord {
payRetryInfo?: RetryInfo;
lastPayError: TalerErrorDetails | undefined;
lastPayError: TalerErrorDetail | undefined;
/**
* Retry information for querying the refund status with the merchant.
@ -1266,7 +1266,7 @@ export interface PurchaseRecord {
/**
* Last error (or undefined) for querying the refund status with the merchant.
*/
lastRefundStatusError: TalerErrorDetails | undefined;
lastRefundStatusError: TalerErrorDetail | undefined;
/**
* Continue querying the refund status until this deadline has expired.
@ -1400,7 +1400,7 @@ export interface WithdrawalGroupRecord {
*/
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
export interface BankWithdrawUriRecord {
@ -1465,7 +1465,7 @@ export interface RecoupGroupRecord {
/**
* Last error that occurred, if any.
*/
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
export enum BackupProviderStateTag {
@ -1485,7 +1485,7 @@ export type BackupProviderState =
| {
tag: BackupProviderStateTag.Retrying;
retryInfo: RetryInfo;
lastError?: TalerErrorDetails;
lastError?: TalerErrorDetail;
};
export interface BackupProviderTerms {
@ -1598,7 +1598,7 @@ export interface DepositGroupRecord {
operationStatus: OperationStatus;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
/**
* Retry info.

View File

@ -23,63 +23,143 @@
/**
* Imports.
*/
import { TalerErrorCode, TalerErrorDetails } from "@gnu-taler/taler-util";
import {
TalerErrorCode,
TalerErrorDetail,
TransactionType,
} from "@gnu-taler/taler-util";
/**
* This exception is there to let the caller know that an error happened,
* but the error has already been reported by writing it to the database.
*/
export class OperationFailedAndReportedError extends Error {
static fromCode(
ec: TalerErrorCode,
message: string,
details: Record<string, unknown>,
): OperationFailedAndReportedError {
return new OperationFailedAndReportedError(
makeErrorDetails(ec, message, details),
);
}
constructor(public operationError: TalerErrorDetails) {
super(operationError.message);
// Set the prototype explicitly.
Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
}
}
/**
* This exception is thrown when an error occurred and the caller is
* responsible for recording the failure in the database.
*/
export class OperationFailedError extends Error {
static fromCode(
ec: TalerErrorCode,
message: string,
details: Record<string, unknown>,
): OperationFailedError {
return new OperationFailedError(makeErrorDetails(ec, message, details));
}
constructor(public operationError: TalerErrorDetails) {
super(operationError.message);
// Set the prototype explicitly.
Object.setPrototypeOf(this, OperationFailedError.prototype);
}
}
export function makeErrorDetails(
ec: TalerErrorCode,
message: string,
details: Record<string, unknown>,
): TalerErrorDetails {
return {
code: ec,
hint: `Error: ${TalerErrorCode[ec]}`,
details: details,
message,
export interface DetailsMap {
[TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
innerError: TalerErrorDetail;
transactionId?: string;
};
[TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: {
exchangeBaseUrl: string;
};
[TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: {
exchangeProtocolVersion: string;
walletProtocolVersion: string;
};
[TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: {};
[TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID]: {};
[TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
orderId: string;
claimUrl: string;
};
[TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: {};
[TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
merchantPub: string;
orderId: string;
};
[TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: {
baseUrlForDownload: string;
baseUrlFromContractTerms: string;
};
[TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: {
talerPayUri: string;
};
[TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {};
[TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {};
[TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {};
[TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {};
[TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {};
[TalerErrorCode.WALLET_NETWORK_ERROR]: {};
[TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {};
[TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: {};
[TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {};
[TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {};
[TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {};
}
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : never;
export function makeErrorDetail<C extends TalerErrorCode>(
code: C,
detail: ErrBody<C>,
hint?: string,
): TalerErrorDetail {
// FIXME: include default hint?
return { code, hint, ...detail };
}
export function makePendingOperationFailedError(
innerError: TalerErrorDetail,
tag: TransactionType,
uid: string,
): TalerError {
return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, {
innerError,
transactionId: `${tag}:${uid}`,
});
}
export class TalerError<T = any> extends Error {
errorDetail: TalerErrorDetail & T;
private constructor(d: TalerErrorDetail & T) {
super();
this.errorDetail = d;
Object.setPrototypeOf(this, TalerError.prototype);
}
static fromDetail<C extends TalerErrorCode>(
code: C,
detail: ErrBody<C>,
hint?: string,
): TalerError {
// FIXME: include default hint?
return new TalerError<unknown>({ code, hint, ...detail });
}
static fromUncheckedDetail(d: TalerErrorDetail): TalerError {
return new TalerError<unknown>({ ...d });
}
static fromException(e: any): TalerError {
const errDetail = getErrorDetailFromException(e);
return new TalerError(errDetail);
}
hasErrorCode<C extends keyof DetailsMap>(
code: C,
): this is TalerError<DetailsMap[C]> {
return this.errorDetail.code === code;
}
}
/**
* Convert an exception (or anything that was thrown) into
* a TalerErrorDetail object.
*/
export function getErrorDetailFromException(e: any): TalerErrorDetail {
if (e instanceof TalerError) {
return e.errorDetail;
}
if (e instanceof Error) {
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
{
stack: e.stack,
},
`unexpected exception (message: ${e.message})`,
);
return err;
}
// Something was thrown that is not even an exception!
// Try to stringify it.
let excString: string;
try {
excString = e.toString();
} catch (e) {
// Something went horribly wrong.
excString = "can't stringify exception";
}
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
{},
`unexpected exception (not an exception, ${excString})`,
);
return err;
}
/**
@ -89,44 +169,24 @@ export function makeErrorDetails(
*/
export async function guardOperationException<T>(
op: () => Promise<T>,
onOpError: (e: TalerErrorDetails) => Promise<void>,
onOpError: (e: TalerErrorDetail) => Promise<void>,
): Promise<T> {
try {
return await op();
} catch (e: any) {
if (e instanceof OperationFailedAndReportedError) {
if (
e instanceof TalerError &&
e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED)
) {
throw e;
}
if (e instanceof OperationFailedError) {
await onOpError(e.operationError);
throw new OperationFailedAndReportedError(e.operationError);
}
if (e instanceof Error) {
const opErr = makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
`unexpected exception (message: ${e.message})`,
{
stack: e.stack,
},
);
await onOpError(opErr);
throw new OperationFailedAndReportedError(opErr);
}
// Something was thrown that is not even an exception!
// Try to stringify it.
let excString: string;
try {
excString = e.toString();
} catch (e) {
// Something went horribly wrong.
excString = "can't stringify exception";
}
const opErr = makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
`unexpected exception (not an exception, ${excString})`,
{},
);
const opErr = getErrorDetailFromException(e);
await onOpError(opErr);
throw new OperationFailedAndReportedError(opErr);
throw TalerError.fromDetail(
TalerErrorCode.WALLET_PENDING_OPERATION_FAILED,
{
innerError: e.errorDetail,
},
);
}
}

View File

@ -27,7 +27,7 @@ import {
} from "../util/http.js";
import { RequestThrottler } from "@gnu-taler/taler-util";
import Axios, { AxiosResponse } from "axios";
import { OperationFailedError, makeErrorDetails } from "../errors.js";
import { TalerError } from "../errors.js";
import { Logger, bytesToString } from "@gnu-taler/taler-util";
import { TalerErrorCode, URL } from "@gnu-taler/taler-util";
@ -55,14 +55,14 @@ export class NodeHttpLib implements HttpRequestLibrary {
const parsedUrl = new URL(url);
if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
`request to origin ${parsedUrl.origin} was throttled`,
{
requestMethod: method,
requestUrl: url,
throttleStats: this.throttle.getThrottleStats(url),
},
`request to origin ${parsedUrl.origin} was throttled`,
);
}
let timeout: number | undefined;
@ -83,13 +83,13 @@ export class NodeHttpLib implements HttpRequestLibrary {
maxRedirects: 0,
});
} catch (e: any) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_NETWORK_ERROR,
`${e.message}`,
{
requestUrl: url,
requestMethod: method,
},
`${e.message}`,
);
}
@ -105,30 +105,26 @@ export class NodeHttpLib implements HttpRequestLibrary {
responseJson = JSON.parse(respText);
} catch (e) {
logger.trace(`invalid json: '${resp.data}'`);
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"invalid JSON",
{
httpStatusCode: resp.status,
requestUrl: url,
requestMethod: method,
},
),
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
httpStatusCode: resp.status,
requestUrl: url,
requestMethod: method,
},
"Could not parse response body as JSON",
);
}
if (responseJson === null || typeof responseJson !== "object") {
logger.trace(`invalid json (not an object): '${respText}'`);
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"invalid JSON",
{
httpStatusCode: resp.status,
requestUrl: url,
requestMethod: method,
},
),
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
httpStatusCode: resp.status,
requestUrl: url,
requestMethod: method,
},
`invalid JSON`,
);
}
return responseJson;

View File

@ -48,7 +48,7 @@ import {
PreparePayResultType,
RecoveryLoadRequest,
RecoveryMergeStrategy,
TalerErrorDetails,
TalerErrorDetail,
AbsoluteTime,
URL,
WalletBackupContentV1,
@ -464,7 +464,7 @@ async function incrementBackupRetryInTx(
backupProviders: typeof WalletStoresV1.backupProviders;
}>,
backupProviderBaseUrl: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
const pr = await tx.backupProviders.get(backupProviderBaseUrl);
if (!pr) {
@ -487,7 +487,7 @@ async function incrementBackupRetryInTx(
async function incrementBackupRetry(
ws: InternalWalletState,
backupProviderBaseUrl: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({ backupProviders: x.backupProviders }))
@ -509,7 +509,7 @@ export async function processBackupForProvider(
throw Error("unknown backup provider");
}
const onOpErr = (err: TalerErrorDetails): Promise<void> =>
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
incrementBackupRetry(ws, backupProviderBaseUrl, err);
const run = async () => {
@ -700,7 +700,7 @@ export interface ProviderInfo {
/**
* Last communication issue with the provider.
*/
lastError?: TalerErrorDetails;
lastError?: TalerErrorDetail;
lastSuccessfulBackupTimestamp?: TalerProtocolTimestamp;
lastAttemptedBackupTimestamp?: TalerProtocolTimestamp;
paymentProposalIds: string[];

View File

@ -36,7 +36,7 @@ import {
Logger,
NotificationType,
parsePaytoUri,
TalerErrorDetails,
TalerErrorDetail,
TalerProtocolTimestamp,
TrackDepositGroupRequest,
TrackDepositGroupResponse,
@ -83,7 +83,7 @@ async function resetDepositGroupRetry(
async function incrementDepositRetry(
ws: InternalWalletState,
depositGroupId: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({ depositGroups: x.depositGroups }))
@ -111,7 +111,7 @@ export async function processDepositGroup(
forceNow = false,
): Promise<void> {
await ws.memoProcessDeposit.memo(depositGroupId, async () => {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementDepositRetry(ws, depositGroupId, e);
return await guardOperationException(
async () => await processDepositGroupImpl(ws, depositGroupId, forceNow),

View File

@ -34,7 +34,7 @@ import {
Recoup,
TalerErrorCode,
URL,
TalerErrorDetails,
TalerErrorDetail,
AbsoluteTime,
hashDenomPub,
LibtoolVersion,
@ -64,11 +64,7 @@ import {
} from "../util/http.js";
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import {
guardOperationException,
makeErrorDetails,
OperationFailedError,
} from "../errors.js";
import { guardOperationException, TalerError } from "../errors.js";
import { InternalWalletState, TrustInfo } from "../common.js";
import {
WALLET_CACHE_BREAKER_CLIENT_VERSION,
@ -112,7 +108,7 @@ function denominationRecordFromKeys(
async function handleExchangeUpdateError(
ws: InternalWalletState,
baseUrl: string,
err: TalerErrorDetails,
err: TalerErrorDetail,
): Promise<void> {
await ws.db
.mktx((x) => ({ exchanges: x.exchanges }))
@ -353,7 +349,7 @@ export async function updateExchangeFromUrl(
exchange: ExchangeRecord;
exchangeDetails: ExchangeDetailsRecord;
}> {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
handleExchangeUpdateError(ws, baseUrl, e);
return await guardOperationException(
() => updateExchangeFromUrlImpl(ws, baseUrl, acceptedFormat, forceNow),
@ -429,14 +425,13 @@ async function downloadExchangeKeysInfo(
logger.info("received /keys response");
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
const opErr = makeErrorDetails(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
"exchange doesn't offer any denominations",
{
exchangeBaseUrl: baseUrl,
},
"exchange doesn't offer any denominations",
);
throw new OperationFailedError(opErr);
}
const protocolVersion = exchangeKeysJsonUnchecked.version;
@ -446,15 +441,14 @@ async function downloadExchangeKeysInfo(
protocolVersion,
);
if (versionRes?.compatible != true) {
const opErr = makeErrorDetails(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
"exchange protocol version not compatible with wallet",
{
exchangeProtocolVersion: protocolVersion,
walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
},
"exchange protocol version not compatible with wallet",
);
throw new OperationFailedError(opErr);
}
const currency = Amounts.parseOrThrow(

View File

@ -25,6 +25,7 @@
* Imports.
*/
import {
AbsoluteTime,
AmountJson,
Amounts,
codecForContractTerms,
@ -34,7 +35,6 @@ import {
ConfirmPayResult,
ConfirmPayResultType,
ContractTerms,
decodeCrock,
Duration,
durationMax,
durationMin,
@ -43,19 +43,17 @@ import {
getRandomBytes,
HttpStatusCode,
j2s,
kdf,
Logger,
NotificationType,
parsePayUri,
PreparePayResult,
PreparePayResultType,
RefreshReason,
stringToBytes,
TalerErrorCode,
TalerErrorDetails,
AbsoluteTime,
URL,
TalerErrorDetail,
TalerProtocolTimestamp,
TransactionType,
URL,
} from "@gnu-taler/taler-util";
import { EXCHANGE_COINS_LOCK, InternalWalletState } from "../common.js";
import {
@ -74,9 +72,9 @@ import {
} from "../db.js";
import {
guardOperationException,
makeErrorDetails,
OperationFailedAndReportedError,
OperationFailedError,
makeErrorDetail,
makePendingOperationFailedError,
TalerError,
} from "../errors.js";
import {
AvailableCoinInfo,
@ -467,7 +465,7 @@ async function recordConfirmPay(
async function reportProposalError(
ws: InternalWalletState,
proposalId: string,
err: TalerErrorDetails,
err: TalerErrorDetail,
): Promise<void> {
await ws.db
.mktx((x) => ({ proposals: x.proposals }))
@ -550,7 +548,7 @@ async function incrementPurchasePayRetry(
async function reportPurchasePayError(
ws: InternalWalletState,
proposalId: string,
err: TalerErrorDetails,
err: TalerErrorDetail,
): Promise<void> {
await ws.db
.mktx((x) => ({ purchases: x.purchases }))
@ -575,7 +573,7 @@ export async function processDownloadProposal(
proposalId: string,
forceNow = false,
): Promise<void> {
const onOpErr = (err: TalerErrorDetails): Promise<void> =>
const onOpErr = (err: TalerErrorDetail): Promise<void> =>
reportProposalError(ws, proposalId, err);
await guardOperationException(
() => processDownloadProposalImpl(ws, proposalId, forceNow),
@ -602,7 +600,7 @@ async function resetDownloadProposalRetry(
async function failProposalPermanently(
ws: InternalWalletState,
proposalId: string,
err: TalerErrorDetails,
err: TalerErrorDetail,
): Promise<void> {
await ws.db
.mktx((x) => ({ proposals: x.proposals }))
@ -727,13 +725,13 @@ async function processDownloadProposalImpl(
if (r.isError) {
switch (r.talerErrorResponse.code) {
case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED:
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED,
"order already claimed (likely by other wallet)",
{
orderId: proposal.orderId,
claimUrl: orderClaimUrl,
},
"order already claimed (likely by other wallet)",
);
default:
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
@ -758,13 +756,17 @@ async function processDownloadProposalImpl(
logger.trace(
`malformed contract terms: ${j2s(proposalResp.contract_terms)}`,
);
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
"validation for well-formedness failed",
{},
"validation for well-formedness failed",
);
await failProposalPermanently(ws, proposalId, err);
throw new OperationFailedAndReportedError(err);
throw makePendingOperationFailedError(
err,
TransactionType.Payment,
proposalId,
);
}
const contractTermsHash = ContractTermsUtil.hashContractTerms(
@ -780,13 +782,17 @@ async function processDownloadProposalImpl(
proposalResp.contract_terms,
);
} catch (e) {
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
`schema validation failed: ${e}`,
{},
`schema validation failed: ${e}`,
);
await failProposalPermanently(ws, proposalId, err);
throw new OperationFailedAndReportedError(err);
throw makePendingOperationFailedError(
err,
TransactionType.Payment,
proposalId,
);
}
const sigValid = await ws.cryptoApi.isValidContractTermsSignature(
@ -796,16 +802,20 @@ async function processDownloadProposalImpl(
);
if (!sigValid) {
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID,
"merchant's signature on contract terms is invalid",
{
merchantPub: parsedContractTerms.merchant_pub,
orderId: parsedContractTerms.order_id,
},
"merchant's signature on contract terms is invalid",
);
await failProposalPermanently(ws, proposalId, err);
throw new OperationFailedAndReportedError(err);
throw makePendingOperationFailedError(
err,
TransactionType.Payment,
proposalId,
);
}
const fulfillmentUrl = parsedContractTerms.fulfillment_url;
@ -814,16 +824,20 @@ async function processDownloadProposalImpl(
const baseUrlFromContractTerms = parsedContractTerms.merchant_base_url;
if (baseUrlForDownload !== baseUrlFromContractTerms) {
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH,
"merchant base URL mismatch",
{
baseUrlForDownload,
baseUrlFromContractTerms,
},
"merchant base URL mismatch",
);
await failProposalPermanently(ws, proposalId, err);
throw new OperationFailedAndReportedError(err);
throw makePendingOperationFailedError(
err,
TransactionType.Payment,
proposalId,
);
}
const contractData = extractContractData(
@ -895,10 +909,8 @@ async function startDownloadProposal(
]);
});
/**
* If we have already claimed this proposal with the same sessionId
* nonce and claim token, reuse it.
*/
/* If we have already claimed this proposal with the same sessionId
* nonce and claim token, reuse it. */
if (
oldProposal &&
oldProposal.downloadSessionId === sessionId &&
@ -1029,7 +1041,7 @@ async function storePayReplaySuccess(
async function handleInsufficientFunds(
ws: InternalWalletState,
proposalId: string,
err: TalerErrorDetails,
err: TalerErrorDetail,
): Promise<void> {
logger.trace("handling insufficient funds, trying to re-select coins");
@ -1319,12 +1331,12 @@ export async function preparePayForUri(
const uriResult = parsePayUri(talerPayUri);
if (!uriResult) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
`invalid taler://pay URI (${talerPayUri})`,
{
talerPayUri,
},
`invalid taler://pay URI (${talerPayUri})`,
);
}
@ -1503,7 +1515,7 @@ export async function processPurchasePay(
proposalId: string,
forceNow = false,
): Promise<ConfirmPayResult> {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
reportPurchasePayError(ws, proposalId, e);
return await guardOperationException(
() => processPurchasePayImpl(ws, proposalId, forceNow),
@ -1527,9 +1539,8 @@ async function processPurchasePayImpl(
lastError: {
// FIXME: allocate more specific error code
code: TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
message: `trying to pay for purchase that is not in the database`,
hint: `proposal ID is ${proposalId}`,
details: {},
hint: `trying to pay for purchase that is not in the database`,
proposalId: proposalId,
},
};
}
@ -1594,10 +1605,10 @@ async function processPurchasePayImpl(
resp.status <= 599
) {
logger.trace("treating /pay error as transient");
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
"/pay failed",
getHttpResponseErrorDetails(resp),
"/pay failed",
);
return {
type: ConfirmPayResultType.Pending,
@ -1621,8 +1632,11 @@ async function processPurchasePayImpl(
delete purch.payRetryInfo;
await tx.purchases.put(purch);
});
// FIXME: Maybe introduce a new return type for this instead of throwing?
throw new OperationFailedAndReportedError(errDetails);
throw makePendingOperationFailedError(
errDetails,
TransactionType.Payment,
proposalId,
);
}
if (resp.status === HttpStatusCode.Conflict) {
@ -1692,10 +1706,10 @@ async function processPurchasePayImpl(
resp.status >= 500 &&
resp.status <= 599
) {
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
"/paid failed",
getHttpResponseErrorDetails(resp),
"/paid failed",
);
return {
type: ConfirmPayResultType.Pending,
@ -1703,10 +1717,10 @@ async function processPurchasePayImpl(
};
}
if (resp.status !== 204) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
"/paid failed",
getHttpResponseErrorDetails(resp),
"/paid failed",
);
}
await storePayReplaySuccess(ws, proposalId, sessionId);

View File

@ -30,7 +30,7 @@ import {
j2s,
NotificationType,
RefreshReason,
TalerErrorDetails,
TalerErrorDetail,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
@ -60,7 +60,7 @@ const logger = new Logger("operations/recoup.ts");
async function incrementRecoupRetry(
ws: InternalWalletState,
recoupGroupId: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({
@ -384,7 +384,7 @@ export async function processRecoupGroup(
forceNow = false,
): Promise<void> {
await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementRecoupRetry(ws, recoupGroupId, e);
return await guardOperationException(
async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),

View File

@ -43,7 +43,7 @@ import {
NotificationType,
RefreshGroupId,
RefreshReason,
TalerErrorDetails,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { amountToPretty } from "@gnu-taler/taler-util";
@ -714,7 +714,7 @@ async function refreshReveal(
async function incrementRefreshRetry(
ws: InternalWalletState,
refreshGroupId: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({
@ -747,7 +747,7 @@ export async function processRefreshGroup(
forceNow = false,
): Promise<void> {
await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementRefreshRetry(ws, refreshGroupId, e);
return await guardOperationException(
async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),

View File

@ -40,7 +40,7 @@ import {
parseRefundUri,
RefreshReason,
TalerErrorCode,
TalerErrorDetails,
TalerErrorDetail,
URL,
codecForMerchantOrderStatusPaid,
AbsoluteTime,
@ -88,7 +88,7 @@ async function resetPurchaseQueryRefundRetry(
async function incrementPurchaseQueryRefundRetry(
ws: InternalWalletState,
proposalId: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({
@ -592,7 +592,7 @@ export async function processPurchaseQueryRefund(
proposalId: string,
forceNow = false,
): Promise<void> {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementPurchaseQueryRefundRetry(ws, proposalId, e);
await guardOperationException(
() => processPurchaseQueryRefundImpl(ws, proposalId, forceNow, true),

View File

@ -34,7 +34,7 @@ import {
NotificationType,
randomBytes,
TalerErrorCode,
TalerErrorDetails,
TalerErrorDetail,
AbsoluteTime,
URL,
} from "@gnu-taler/taler-util";
@ -47,7 +47,7 @@ import {
WalletStoresV1,
WithdrawalGroupRecord,
} from "../db.js";
import { guardOperationException, OperationFailedError } from "../errors.js";
import { guardOperationException, TalerError } from "../errors.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import {
readSuccessResponseJsonOrErrorCode,
@ -135,7 +135,7 @@ async function incrementReserveRetry(
async function reportReserveError(
ws: InternalWalletState,
reservePub: string,
err: TalerErrorDetails,
err: TalerErrorDetail,
): Promise<void> {
await ws.db
.mktx((x) => ({
@ -338,7 +338,7 @@ export async function processReserve(
forceNow = false,
): Promise<void> {
return ws.memoProcessReserve.memo(reservePub, async () => {
const onOpError = (err: TalerErrorDetails): Promise<void> =>
const onOpError = (err: TalerErrorDetail): Promise<void> =>
reportReserveError(ws, reservePub, err);
await guardOperationException(
() => processReserveImpl(ws, reservePub, forceNow),
@ -571,7 +571,7 @@ async function updateReserve(
if (
resp.status === 404 &&
result.talerErrorResponse.code ===
TalerErrorCode.EXCHANGE_RESERVES_GET_STATUS_UNKNOWN
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) {
ws.notify({
type: NotificationType.ReserveNotYetFound,
@ -803,9 +803,8 @@ export async function createTalerWithdrawReserve(
return tx.reserves.get(reserve.reservePub);
});
if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
"withdrawal aborted by bank",
{},
);
}

View File

@ -22,7 +22,7 @@ import {
parseTipUri,
codecForTipPickupGetResponse,
Amounts,
TalerErrorDetails,
TalerErrorDetail,
NotificationType,
TipPlanchetDetail,
TalerErrorCode,
@ -44,7 +44,7 @@ import {
import { j2s } from "@gnu-taler/taler-util";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { guardOperationException, makeErrorDetails } from "../errors.js";
import { guardOperationException, makeErrorDetail } from "../errors.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import { InternalWalletState } from "../common.js";
import {
@ -163,7 +163,7 @@ export async function prepareTip(
async function incrementTipRetry(
ws: InternalWalletState,
walletTipId: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({
@ -192,7 +192,7 @@ export async function processTip(
tipId: string,
forceNow = false,
): Promise<void> {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementTipRetry(ws, tipId, e);
await guardOperationException(
() => processTipImpl(ws, tipId, forceNow),
@ -296,10 +296,10 @@ async function processTipImpl(
merchantResp.status === 424)
) {
logger.trace(`got transient tip error`);
const err = makeErrorDetails(
const err = makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
"tip pickup failed (transient)",
getHttpResponseErrorDetails(merchantResp),
"tip pickup failed (transient)",
);
await incrementTipRetry(ws, tipRecord.walletTipId, err);
// FIXME: Maybe we want to signal to the caller that the transient error happened?
@ -355,10 +355,10 @@ async function processTipImpl(
if (!tipRecord) {
return;
}
tipRecord.lastError = makeErrorDetails(
tipRecord.lastError = makeErrorDetail(
TalerErrorCode.WALLET_TIPPING_COIN_SIGNATURE_INVALID,
"invalid signature from the exchange (via merchant tip) after unblinding",
{},
"invalid signature from the exchange (via merchant tip) after unblinding",
);
await tx.tips.put(tipRecord);
});

View File

@ -30,7 +30,7 @@ import {
NotificationType,
parseWithdrawUri,
TalerErrorCode,
TalerErrorDetails,
TalerErrorDetail,
AbsoluteTime,
WithdrawResponse,
URL,
@ -42,6 +42,7 @@ import {
ExchangeWithdrawRequest,
Duration,
TalerProtocolTimestamp,
TransactionType,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
@ -63,9 +64,11 @@ import {
} from "../util/http.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import {
getErrorDetailFromException,
guardOperationException,
makeErrorDetails,
OperationFailedError,
makeErrorDetail,
makePendingOperationFailedError,
TalerError,
} from "../errors.js";
import { InternalWalletState } from "../common.js";
import {
@ -299,15 +302,14 @@ export async function getBankWithdrawalInfo(
config.version,
);
if (versionRes?.compatible != true) {
const opErr = makeErrorDetails(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
"bank integration protocol version not compatible with wallet",
{
exchangeProtocolVersion: config.version,
walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
},
"bank integration protocol version not compatible with wallet",
);
throw new OperationFailedError(opErr);
}
const reqUrl = new URL(
@ -526,12 +528,9 @@ async function processPlanchetExchangeRequest(
);
return r;
} catch (e) {
const errDetail = getErrorDetailFromException(e);
logger.trace("withdrawal request failed", e);
logger.trace(e);
if (!(e instanceof OperationFailedError)) {
throw e;
}
const errDetails = e.operationError;
await ws.db
.mktx((x) => ({ planchets: x.planchets }))
.runReadWrite(async (tx) => {
@ -542,7 +541,7 @@ async function processPlanchetExchangeRequest(
if (!planchet) {
return;
}
planchet.lastError = errDetails;
planchet.lastError = errDetail;
await tx.planchets.put(planchet);
});
return;
@ -628,10 +627,10 @@ async function processPlanchetVerifyAndStoreCoin(
if (!planchet) {
return;
}
planchet.lastError = makeErrorDetails(
planchet.lastError = makeErrorDetail(
TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
"invalid signature from the exchange after unblinding",
{},
"invalid signature from the exchange after unblinding",
);
await tx.planchets.put(planchet);
});
@ -797,7 +796,7 @@ export async function updateWithdrawalDenoms(
async function incrementWithdrawalRetry(
ws: InternalWalletState,
withdrawalGroupId: string,
err: TalerErrorDetails | undefined,
err: TalerErrorDetail | undefined,
): Promise<void> {
await ws.db
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
@ -821,7 +820,7 @@ export async function processWithdrawGroup(
withdrawalGroupId: string,
forceNow = false,
): Promise<void> {
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
const onOpErr = (e: TalerErrorDetail): Promise<void> =>
incrementWithdrawalRetry(ws, withdrawalGroupId, e);
await guardOperationException(
() => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
@ -919,7 +918,7 @@ async function processWithdrawGroupImpl(
let numFinished = 0;
let finishedForFirstTime = false;
let errorsPerCoin: Record<number, TalerErrorDetails> = {};
let errorsPerCoin: Record<number, TalerErrorDetail> = {};
await ws.db
.mktx((x) => ({
@ -957,12 +956,12 @@ async function processWithdrawGroupImpl(
});
if (numFinished != numTotalCoins) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
`withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
{
errorsPerCoin,
},
`withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
);
}

View File

@ -25,7 +25,7 @@
* Imports.
*/
import {
TalerErrorDetails,
TalerErrorDetail,
BalancesResponse,
AbsoluteTime,
TalerProtocolTimestamp,
@ -71,7 +71,7 @@ export type PendingTaskInfo = PendingTaskInfoCommon &
export interface PendingBackupTask {
type: PendingTaskType.Backup;
backupProviderBaseUrl: string;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
/**
@ -80,7 +80,7 @@ export interface PendingBackupTask {
export interface PendingExchangeUpdateTask {
type: PendingTaskType.ExchangeUpdate;
exchangeBaseUrl: string;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
/**
@ -124,7 +124,7 @@ export interface PendingReserveTask {
*/
export interface PendingRefreshTask {
type: PendingTaskType.Refresh;
lastError?: TalerErrorDetails;
lastError?: TalerErrorDetail;
refreshGroupId: string;
finishedPerCoin: boolean[];
retryInfo: RetryInfo;
@ -139,7 +139,7 @@ export interface PendingProposalDownloadTask {
proposalTimestamp: TalerProtocolTimestamp;
proposalId: string;
orderId: string;
lastError?: TalerErrorDetails;
lastError?: TalerErrorDetail;
retryInfo?: RetryInfo;
}
@ -173,7 +173,7 @@ export interface PendingPayTask {
proposalId: string;
isReplay: boolean;
retryInfo?: RetryInfo;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
/**
@ -184,14 +184,14 @@ export interface PendingRefundQueryTask {
type: PendingTaskType.RefundQuery;
proposalId: string;
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
export interface PendingRecoupTask {
type: PendingTaskType.Recoup;
recoupGroupId: string;
retryInfo: RetryInfo;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
}
/**
@ -199,7 +199,7 @@ export interface PendingRecoupTask {
*/
export interface PendingWithdrawTask {
type: PendingTaskType.Withdraw;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
retryInfo: RetryInfo;
withdrawalGroupId: string;
}
@ -209,7 +209,7 @@ export interface PendingWithdrawTask {
*/
export interface PendingDepositTask {
type: PendingTaskType.Deposit;
lastError: TalerErrorDetails | undefined;
lastError: TalerErrorDetail | undefined;
retryInfo: RetryInfo | undefined;
depositGroupId: string;
}

View File

@ -24,16 +24,16 @@
/**
* Imports
*/
import { OperationFailedError, makeErrorDetails } from "../errors.js";
import {
Logger,
Duration,
AbsoluteTime,
TalerErrorDetails,
TalerErrorDetail,
Codec,
j2s,
} from "@gnu-taler/taler-util";
import { TalerErrorCode } from "@gnu-taler/taler-util";
import { makeErrorDetail, TalerError } from "../errors.js";
const logger = new Logger("http.ts");
@ -125,7 +125,7 @@ type ResponseOrError<T> =
export async function readTalerErrorResponse(
httpResponse: HttpResponse,
): Promise<TalerErrorDetails> {
): Promise<TalerErrorDetail> {
const errJson = await httpResponse.json();
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
@ -134,16 +134,14 @@ export async function readTalerErrorResponse(
errJson,
)}`,
);
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Error response did not contain error code",
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
},
),
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
},
"Error response did not contain error code",
);
}
return errJson;
@ -151,28 +149,28 @@ export async function readTalerErrorResponse(
export async function readUnexpectedResponseDetails(
httpResponse: HttpResponse,
): Promise<TalerErrorDetails> {
): Promise<TalerErrorDetail> {
const errJson = await httpResponse.json();
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
return makeErrorDetails(
return makeErrorDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Error response did not contain error code",
{
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
httpStatusCode: httpResponse.status,
},
"Error response did not contain error code",
);
}
return makeErrorDetails(
return makeErrorDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
`Unexpected HTTP status (${httpResponse.status}) in response`,
{
requestUrl: httpResponse.requestUrl,
httpStatusCode: httpResponse.status,
errorResponse: errJson,
},
`Unexpected HTTP status (${httpResponse.status}) in response`,
);
}
@ -191,14 +189,14 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
try {
parsedResponse = codec.decode(respJson);
} catch (e: any) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Response invalid",
{
requestUrl: httpResponse.requestUrl,
httpStatusCode: httpResponse.status,
validationError: e.toString(),
},
"Response invalid",
);
}
return {
@ -220,16 +218,14 @@ export function throwUnexpectedRequestError(
httpResponse: HttpResponse,
talerErrorResponse: TalerErrorResponse,
): never {
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
`Unexpected HTTP status ${httpResponse.status} in response`,
{
requestUrl: httpResponse.requestUrl,
httpStatusCode: httpResponse.status,
errorResponse: talerErrorResponse,
},
),
throw TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: httpResponse.requestUrl,
httpStatusCode: httpResponse.status,
errorResponse: talerErrorResponse,
},
`Unexpected HTTP status ${httpResponse.status} in response`,
);
}
@ -251,16 +247,14 @@ export async function readSuccessResponseTextOrErrorCode<T>(
const errJson = await httpResponse.json();
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Error response did not contain error code",
{
httpStatusCode: httpResponse.status,
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
},
),
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
httpStatusCode: httpResponse.status,
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
},
"Error response did not contain error code",
);
}
return {
@ -282,16 +276,14 @@ export async function checkSuccessResponseOrThrow(
const errJson = await httpResponse.json();
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Error response did not contain error code",
{
httpStatusCode: httpResponse.status,
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
},
),
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
httpStatusCode: httpResponse.status,
requestUrl: httpResponse.requestUrl,
requestMethod: httpResponse.requestMethod,
},
"Error response did not contain error code",
);
}
throwUnexpectedRequestError(httpResponse, errJson);

View File

@ -100,9 +100,8 @@ import {
WalletStoresV1,
} from "./db.js";
import {
makeErrorDetails,
OperationFailedAndReportedError,
OperationFailedError,
getErrorDetailFromException,
TalerError,
} from "./errors.js";
import { exportBackup } from "./operations/backup/export.js";
import {
@ -297,10 +296,10 @@ export async function runPending(
try {
await processOnePendingOperation(ws, p, forceNow);
} catch (e) {
if (e instanceof OperationFailedAndReportedError) {
if (e instanceof TalerError) {
console.error(
"Operation failed:",
JSON.stringify(e.operationError, undefined, 2),
JSON.stringify(e.errorDetail, undefined, 2),
);
} else {
console.error(e);
@ -399,10 +398,16 @@ async function runTaskLoop(
try {
await processOnePendingOperation(ws, p);
} catch (e) {
if (e instanceof OperationFailedAndReportedError) {
logger.warn("operation processed resulted in reported error");
logger.warn(`reported error was: ${j2s(e.operationError)}`);
if (
e instanceof TalerError &&
e.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED)
) {
logger.warn("operation processed resulted in error");
logger.warn(`error was: ${j2s(e.errorDetail)}`);
} else {
// This is a bug, as we expect pending operations to always
// do their own error handling and only throw WALLET_PENDING_OPERATION_FAILED
// or return something.
logger.error("Uncaught exception", e);
ws.notify({
type: NotificationType.InternalError,
@ -722,7 +727,7 @@ export async function getClientFromWalletState(
const res = await handleCoreApiRequest(ws, op, `${id++}`, payload);
switch (res.type) {
case "error":
throw new OperationFailedError(res.error);
throw TalerError.fromUncheckedDetail(res.error);
case "response":
return res.result;
}
@ -1040,12 +1045,12 @@ async function dispatchRequestInternal(
return [];
}
}
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
"unknown operation",
{
operation,
},
"unknown operation",
);
}
@ -1067,34 +1072,13 @@ export async function handleCoreApiRequest(
result,
};
} catch (e: any) {
if (
e instanceof OperationFailedError ||
e instanceof OperationFailedAndReportedError
) {
logger.error("Caught operation failed error");
logger.trace((e as any).stack);
return {
type: "error",
operation,
id,
error: e.operationError,
};
} else {
try {
logger.error("Caught unexpected exception:");
logger.error(e.stack);
} catch (e) {}
return {
type: "error",
operation,
id,
error: makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
`unexpected exception: ${e}`,
{},
),
};
}
const err = getErrorDetailFromException(e);
return {
type: "error",
operation,
id,
error: err,
};
}
}

View File

@ -21,7 +21,6 @@ import {
getDefaultNodeWallet,
DefaultNodeWalletArgs,
NodeHttpLib,
makeErrorDetails,
handleWorkerError,
handleWorkerMessage,
HttpRequestLibrary,
@ -33,6 +32,7 @@ import {
WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION,
Wallet,
getErrorDetailFromException,
} from "@gnu-taler/taler-wallet-core";
import fs from "fs";
@ -270,11 +270,7 @@ export function installNativeWalletListener(): void {
type: "error",
id,
operation,
error: makeErrorDetails(
TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
"unexpected exception",
{},
),
error: getErrorDetailFromException(e),
};
sendNativeMessage(respMsg);
return;

View File

@ -18,11 +18,11 @@
* Imports.
*/
import {
OperationFailedError,
HttpRequestLibrary,
HttpRequestOptions,
HttpResponse,
Headers,
TalerError,
} from "@gnu-taler/taler-wallet-core";
import {
Logger,
@ -49,14 +49,14 @@ export class BrowserHttpLib implements HttpRequestLibrary {
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
const parsedUrl = new URL(requestUrl);
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
`request to origin ${parsedUrl.origin} was throttled`,
{
requestMethod,
requestUrl,
throttleStats: this.throttle.getThrottleStats(requestUrl),
},
`request to origin ${parsedUrl.origin} was throttled`,
);
}
@ -78,12 +78,12 @@ export class BrowserHttpLib implements HttpRequestLibrary {
myRequest.onerror = (e) => {
logger.error("http request error");
reject(
OperationFailedError.fromCode(
TalerError.fromDetail(
TalerErrorCode.WALLET_NETWORK_ERROR,
"Could not make request",
{
requestUrl: requestUrl,
},
"Could not make request",
),
);
};
@ -91,12 +91,12 @@ export class BrowserHttpLib implements HttpRequestLibrary {
myRequest.addEventListener("readystatechange", (e) => {
if (myRequest.readyState === XMLHttpRequest.DONE) {
if (myRequest.status === 0) {
const exc = OperationFailedError.fromCode(
const exc = TalerError.fromDetail(
TalerErrorCode.WALLET_NETWORK_ERROR,
"HTTP request failed (status 0, maybe URI scheme was wrong?)",
{
requestUrl: requestUrl,
},
"HTTP request failed (status 0, maybe URI scheme was wrong?)",
);
reject(exc);
return;
@ -112,23 +112,23 @@ export class BrowserHttpLib implements HttpRequestLibrary {
const responseString = td.decode(myRequest.response);
responseJson = JSON.parse(responseString);
} catch (e) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response",
{
requestUrl: requestUrl,
httpStatusCode: myRequest.status,
},
"Invalid JSON from HTTP response",
);
}
if (responseJson === null || typeof responseJson !== "object") {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response",
{
requestUrl: requestUrl,
httpStatusCode: myRequest.status,
},
"Invalid JSON from HTTP response",
);
}
return responseJson;

View File

@ -13,7 +13,7 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { TalerErrorDetails } from "@gnu-taler/taler-util";
import { TalerErrorDetail } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import arrowDown from "../../static/img/chevron-down.svg";
@ -25,7 +25,7 @@ export function ErrorTalerOperation({
error,
}: {
title?: VNode;
error?: TalerErrorDetails;
error?: TalerErrorDetail;
}): VNode | null {
const { devMode } = useDevContext();
const [showErrorDetail, setShowErrorDetail] = useState(false);
@ -59,7 +59,7 @@ export function ErrorTalerOperation({
<Fragment>
<div style={{ padding: 5, textAlign: "left" }}>
<div>
<b>{error.message}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
<b>{error.hint}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
</div>
</div>
{devMode && (

View File

@ -35,7 +35,7 @@ import {
PreparePayResultType,
Translate,
} from "@gnu-taler/taler-util";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorTalerOperation } from "../components/ErrorTalerOperation";
@ -64,9 +64,9 @@ export function DepositPage({ talerPayUri, goBack }: Props): VNode {
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
undefined,
);
const [payErrMsg, setPayErrMsg] = useState<
OperationFailedError | string | undefined
>(undefined);
const [payErrMsg, setPayErrMsg] = useState<TalerError | string | undefined>(
undefined,
);
const balance = useAsyncAsHook(wxApi.getBalance, [
NotificationType.CoinWithdrawn,
@ -97,7 +97,7 @@ export function DepositPage({ talerPayUri, goBack }: Props): VNode {
setPayStatus(p);
} catch (e) {
console.log("Got error while trying to pay", e);
if (e instanceof OperationFailedError) {
if (e instanceof TalerError) {
setPayErrMsg(e);
}
if (e instanceof Error) {
@ -117,7 +117,7 @@ export function DepositPage({ talerPayUri, goBack }: Props): VNode {
}
if (!payStatus) {
if (payErrMsg instanceof OperationFailedError) {
if (payErrMsg instanceof TalerError) {
return (
<WalletAction>
<LogoHeader />
@ -131,7 +131,7 @@ export function DepositPage({ talerPayUri, goBack }: Props): VNode {
Could not get the payment information for this order
</i18n.Translate>
}
error={payErrMsg?.operationError}
error={payErrMsg?.errorDetail}
/>
</section>
</WalletAction>

View File

@ -36,7 +36,7 @@ import {
PreparePayResultType,
Product,
} from "@gnu-taler/taler-util";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorMessage } from "../components/ErrorMessage";
@ -93,7 +93,7 @@ export function PayPage({
undefined,
);
const [payErrMsg, setPayErrMsg] = useState<
OperationFailedError | string | undefined
TalerError | string | undefined
>(undefined);
const hook = useAsyncAsHook(async () => {

View File

@ -25,10 +25,8 @@ import {
AmountJson,
Amounts,
ExchangeListItem,
Translate,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../components/Loading";
@ -52,6 +50,7 @@ import {
import * as wxApi from "../wxApi";
import { TermsOfServiceSection } from "./TermsOfServiceSection";
import { useTranslationContext } from "../context/translation";
import { TalerError } from "@gnu-taler/taler-wallet-core";
interface Props {
talerWithdrawUri?: string;
@ -85,9 +84,9 @@ export function View({
reviewed,
}: ViewProps): VNode {
const { i18n } = useTranslationContext();
const [withdrawError, setWithdrawError] = useState<
OperationFailedError | undefined
>(undefined);
const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
undefined,
);
const [confirmDisabled, setConfirmDisabled] = useState<boolean>(false);
const needsReview = terms.status === "changed" || terms.status === "new";
@ -109,7 +108,7 @@ export function View({
setConfirmDisabled(true);
await onWithdraw();
} catch (e) {
if (e instanceof OperationFailedError) {
if (e instanceof TalerError) {
setWithdrawError(e);
}
setConfirmDisabled(false);
@ -130,7 +129,7 @@ export function View({
Could not finish the withdrawal operation
</i18n.Translate>
}
error={withdrawError.operationError}
error={withdrawError.errorDetail}
/>
)}

View File

@ -13,28 +13,32 @@
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { NotificationType, TalerErrorDetails } from "@gnu-taler/taler-util";
import {
NotificationType,
TalerErrorCode,
TalerErrorDetail,
} from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import * as wxApi from "../wxApi";
import { OperationFailedError } from "@gnu-taler/taler-wallet-core";
interface HookOk<T> {
hasError: false;
response: T;
}
export type HookError = HookGenericError | HookOperationalError
export type HookError = HookGenericError | HookOperationalError;
export interface HookGenericError {
hasError: true;
operational: false,
operational: false;
message: string;
}
export interface HookOperationalError {
hasError: true;
operational: true,
details: TalerErrorDetails;
operational: true;
details: TalerErrorDetail;
}
export type HookResponse<T> = HookOk<T> | HookError | undefined;
@ -51,10 +55,18 @@ export function useAsyncAsHook<T>(
const response = await fn();
setHookResponse({ hasError: false, response });
} catch (e) {
if (e instanceof OperationFailedError) {
setHookResponse({ hasError: true, operational: true, details: e.operationError });
if (e instanceof TalerError) {
setHookResponse({
hasError: true,
operational: true,
details: e.errorDetail,
});
} else if (e instanceof Error) {
setHookResponse({ hasError: true, operational: false, message: e.message });
setHookResponse({
hasError: true,
operational: false,
message: e.message,
});
}
}
}

View File

@ -23,7 +23,7 @@ import {
HttpRequestLibrary,
HttpRequestOptions,
HttpResponse,
OperationFailedError,
TalerError,
} from "@gnu-taler/taler-wallet-core";
/**
@ -44,14 +44,14 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary {
if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
const parsedUrl = new URL(requestUrl);
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
`request to origin ${parsedUrl.origin} was throttled`,
{
requestMethod,
requestUrl,
throttleStats: this.throttle.getThrottleStats(requestUrl),
},
`request to origin ${parsedUrl.origin} was throttled`,
);
}
@ -107,13 +107,13 @@ function makeTextHandler(response: Response, requestUrl: string) {
try {
respText = await response.text();
} catch (e) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response",
{
requestUrl,
httpStatusCode: response.status,
},
"Invalid JSON from HTTP response",
);
}
return respText;
@ -126,23 +126,23 @@ function makeJsonHandler(response: Response, requestUrl: string) {
try {
responseJson = await response.json();
} catch (e) {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response",
{
requestUrl,
httpStatusCode: response.status,
},
"Invalid JSON from HTTP response",
);
}
if (responseJson === null || typeof responseJson !== "object") {
throw OperationFailedError.fromCode(
throw TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Invalid JSON from HTTP response",
{
requestUrl,
httpStatusCode: response.status,
},
"Invalid JSON from HTTP response",
);
}
return responseJson;

View File

@ -23,26 +23,48 @@
*/
import {
AcceptExchangeTosRequest,
AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
AddExchangeRequest, AmountString, ApplyRefundResponse, BalancesResponse, CoinDumpJson, ConfirmPayResult,
CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, DeleteTransactionRequest, ExchangesListRespose,
GetExchangeTosResult, GetExchangeWithdrawalInfo,
AcceptManualWithdrawalResult,
AcceptTipRequest,
AcceptWithdrawalResponse,
AddExchangeRequest,
AmountString,
ApplyRefundResponse,
BalancesResponse,
CoinDumpJson,
ConfirmPayResult,
CoreApiResponse,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
DeleteTransactionRequest,
ExchangesListRespose,
GetExchangeTosResult,
GetExchangeWithdrawalInfo,
GetFeeForDepositRequest,
GetWithdrawalDetailsForUriRequest, KnownBankAccounts, NotificationType, PreparePayResult, PrepareTipRequest,
PrepareTipResult, RetryTransactionRequest,
SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, WithdrawUriInfoResponse
GetWithdrawalDetailsForUriRequest,
KnownBankAccounts,
NotificationType,
PreparePayResult,
PrepareTipRequest,
PrepareTipResult,
RetryTransactionRequest,
SetWalletDeviceIdRequest,
TransactionsResponse,
WalletDiagnostics,
WithdrawUriInfoResponse,
} from "@gnu-taler/taler-util";
import {
AddBackupProviderRequest, BackupInfo, OperationFailedError,
AddBackupProviderRequest,
BackupInfo,
PendingOperationsResponse,
RemoveBackupProviderRequest
RemoveBackupProviderRequest,
TalerError,
} from "@gnu-taler/taler-wallet-core";
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
import { MessageFromBackend } from "./wxBackend";
/**
*
*
* @autor Florian Dold
* @autor sebasjm
*/
@ -88,7 +110,7 @@ async function callBackend(operation: string, payload: any): Promise<any> {
console.log("got response", resp);
const r = resp as CoreApiResponse;
if (r.type === "error") {
reject(new OperationFailedError(r.error));
reject(TalerError.fromUncheckedDetail(r.error));
return;
}
resolve(r.result);
@ -127,15 +149,23 @@ export function resetDb(): Promise<void> {
return callBackend("reset-db", {});
}
export function getFeeForDeposit(depositPaytoUri: string, amount: AmountString): Promise<DepositFee> {
export function getFeeForDeposit(
depositPaytoUri: string,
amount: AmountString,
): Promise<DepositFee> {
return callBackend("getFeeForDeposit", {
depositPaytoUri, amount
depositPaytoUri,
amount,
} as GetFeeForDepositRequest);
}
export function createDepositGroup(depositPaytoUri: string, amount: AmountString): Promise<CreateDepositGroupResponse> {
export function createDepositGroup(
depositPaytoUri: string,
amount: AmountString,
): Promise<CreateDepositGroupResponse> {
return callBackend("createDepositGroup", {
depositPaytoUri, amount
depositPaytoUri,
amount,
} as CreateDepositGroupRequest);
}
@ -190,7 +220,9 @@ export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
export function listExchanges(): Promise<ExchangesListRespose> {
return callBackend("listExchanges", {});
}
export function listKnownBankAccounts(currency?: string): Promise<KnownBankAccounts> {
export function listKnownBankAccounts(
currency?: string,
): Promise<KnownBankAccounts> {
return callBackend("listKnownBankAccounts", { currency });
}
@ -387,14 +419,17 @@ export function exportDB(): Promise<any> {
}
export function importDB(dump: any): Promise<void> {
return callBackend("importDb", { dump })
return callBackend("importDb", { dump });
}
export function onUpdateNotification(messageTypes: Array<NotificationType>, doCallback: () => void): () => void {
export function onUpdateNotification(
messageTypes: Array<NotificationType>,
doCallback: () => void,
): () => void {
// eslint-disable-next-line no-undef
const port = chrome.runtime.connect({ name: "notifications" });
const listener = (message: MessageFromBackend): void => {
const shouldNotify = messageTypes.includes(message.type)
const shouldNotify = messageTypes.includes(message.type);
if (shouldNotify) {
doCallback();
}

View File

@ -35,7 +35,7 @@ import {
import {
DbAccess,
deleteTalerDatabase,
makeErrorDetails,
makeErrorDetail,
OpenedPromise,
openPromise,
openTalerDatabase,
@ -167,10 +167,10 @@ async function dispatch(
type: "error",
id: req.id,
operation: req.operation,
error: makeErrorDetails(
error: makeErrorDetail(
TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
"wallet core not available",
{},
"wallet core not available",
),
};
break;
@ -233,7 +233,10 @@ function makeSyncWalletRedirect(
const tab = await getTab(tabId);
if (tab.url === oldUrl) {
console.log("redirecting to", innerUrl.href);
chrome.tabs.update(tabId, { url: innerUrl.href, loadReplace: true } as any);
chrome.tabs.update(tabId, {
url: innerUrl.href,
loadReplace: true,
} as any);
}
};
doit();