/*
This file is part of GNU Taler
(C) 2019-2020 Taler Systems SA
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see
*/
/**
* Classes and helpers for error handling specific to wallet operations.
*
* @author Florian Dold
*/
/**
* Imports.
*/
import {
AbsoluteTime,
CancellationToken,
PaymentInsufficientBalanceDetails,
TalerErrorCode,
TalerErrorDetail,
TransactionType,
} from "@gnu-taler/taler-util";
type empty = Record;
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]: empty;
[TalerErrorCode.WALLET_REWARD_COIN_SIGNATURE_INVALID]: empty;
[TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
orderId: string;
claimUrl: string;
};
[TalerErrorCode.WALLET_ORDER_ALREADY_PAID]: {
orderId: string;
fulfillmentUrl: string;
};
[TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty;
[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]: {
requestUrl: string;
requestMethod: string;
httpStatusCode: number;
errorResponse?: any;
};
[TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {
stack?: string;
};
[TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {
bankProtocolVersion: string;
walletProtocolVersion: string;
};
[TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
operation: string;
};
[TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {
requestUrl: string;
requestMethod: string;
throttleStats: Record;
};
[TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: {
requestUrl: string;
requestMethod: string;
timeoutMs: number;
};
[TalerErrorCode.GENERIC_TIMEOUT]: {
requestUrl: string;
requestMethod: string;
timeoutMs: number;
};
[TalerErrorCode.WALLET_NETWORK_ERROR]: {
requestUrl: string;
requestMethod: string;
};
[TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {
requestUrl: string;
requestMethod: string;
httpStatusCode: number;
validationError?: string;
/**
* Content type of the response, usually only specified if not the
* expected content type.
*/
contentType?: string;
};
[TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR]: {
operation: string;
error: string;
detail: TalerErrorDetail | undefined;
};
[TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
[TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
numErrors: number;
errorsPerCoin: Record;
};
[TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {
lastError?: TalerErrorDetail;
};
[TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {
httpStatusCode: number;
};
[TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
requestError: TalerErrorDetail;
};
[TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: {
innerError: TalerErrorDetail;
};
[TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST]: {
detail: string;
};
[TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED]: {
kycUrl: string;
};
[TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
};
[TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
};
[TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
numErrors: number;
/**
* Errors, can be truncated.
*/
errors: TalerErrorDetail[];
};
[TalerErrorCode.WALLET_EXCHANGE_BASE_URL_MISMATCH]: {
urlWallet: string;
urlExchange: string;
};
[TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE]: {
exchangeBaseUrl: string;
innerError: TalerErrorDetail | undefined;
};
[TalerErrorCode.WALLET_DB_UNAVAILABLE]: {
innerError: TalerErrorDetail | undefined;
};
}
type ErrBody = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
export function makeErrorDetail(
code: C,
detail: ErrBody,
hint?: string,
): TalerErrorDetail {
if (!hint && !(detail as any).hint) {
hint = getDefaultHint(code);
}
const when = AbsoluteTime.now();
return { code, when, 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 function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
const errName = TalerErrorCode[ed.code] ?? "";
return `Error (${ed.code}/${errName})`;
}
function getDefaultHint(code: number): string {
const errName = TalerErrorCode[code];
if (errName) {
return `Error (${errName})`;
} else {
return `Error ()`;
}
}
export class TalerProtocolViolationError extends Error {
constructor(hint?: string) {
let msg: string;
if (hint) {
msg = `Taler protocol violation error (${hint})`;
} else {
msg = `Taler protocol violation error`;
}
super(msg);
Object.setPrototypeOf(this, TalerProtocolViolationError.prototype);
}
}
// compute a subset of TalerError, just for http request
type HttpErrors =
| TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT
| TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED
| TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE
| TalerErrorCode.WALLET_NETWORK_ERROR
| TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR;
type TalerHttpErrorsDetails = {
[code in HttpErrors]: TalerError;
};
export type TalerHttpError =
TalerHttpErrorsDetails[keyof TalerHttpErrorsDetails];
export class TalerError extends Error {
errorDetail: TalerErrorDetail & T;
cause: Error | undefined;
private constructor(d: TalerErrorDetail & T, cause?: Error) {
super(d.hint ?? `Error (code ${d.code})`);
this.errorDetail = d;
this.cause = cause;
Object.setPrototypeOf(this, TalerError.prototype);
}
static fromDetail(
code: C,
detail: ErrBody,
hint?: string,
cause?: Error,
): TalerError {
if (!hint) {
hint = getDefaultHint(code);
}
const when = AbsoluteTime.now();
return new TalerError({ code, when, hint, ...detail }, cause);
}
static fromUncheckedDetail(d: TalerErrorDetail, c?: Error): TalerError {
return new TalerError({ ...d }, c);
}
static fromException(e: any): TalerError {
const errDetail = getErrorDetailFromException(e);
return new TalerError(errDetail, e);
}
hasErrorCode(
code: C,
): this is TalerError {
return this.errorDetail.code === code;
}
toString(): string {
return `TalerError: ${JSON.stringify(this.errorDetail)}`;
}
}
export function safeStringifyException(e: any): string {
return JSON.stringify(getErrorDetailFromException(e), undefined, 2);
}
/**
* 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 CancellationToken.CancellationError) {
const err = makeErrorDetail(
TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED,
{},
);
return err;
}
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;
}
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}