256 lines
7.2 KiB
TypeScript
256 lines
7.2 KiB
TypeScript
/*
|
|
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 <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
/**
|
|
* Classes and helpers for error handling specific to wallet operations.
|
|
*
|
|
* @author Florian Dold <dold@taler.net>
|
|
*/
|
|
|
|
/**
|
|
* Imports.
|
|
*/
|
|
import {
|
|
AbsoluteTime,
|
|
PayMerchantInsufficientBalanceDetails,
|
|
PayPeerInsufficientBalanceDetails,
|
|
TalerErrorCode,
|
|
TalerErrorDetail,
|
|
TransactionType,
|
|
} from "@gnu-taler/taler-util";
|
|
|
|
type empty = Record<string, never>;
|
|
|
|
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_TIPPING_COIN_SIGNATURE_INVALID]: empty;
|
|
[TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
|
|
orderId: string;
|
|
claimUrl: 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]: {
|
|
exchangeProtocolVersion: string;
|
|
walletProtocolVersion: string;
|
|
};
|
|
[TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
|
|
operation: string;
|
|
};
|
|
[TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {
|
|
requestUrl: string;
|
|
requestMethod: string;
|
|
throttleStats: Record<string, unknown>;
|
|
};
|
|
[TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: empty;
|
|
[TalerErrorCode.WALLET_NETWORK_ERROR]: {
|
|
requestUrl: string;
|
|
requestMethod: string;
|
|
};
|
|
[TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {
|
|
requestUrl: string;
|
|
requestMethod: string;
|
|
httpStatusCode: number;
|
|
validationError?: string;
|
|
};
|
|
[TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
|
|
[TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
|
|
errorsPerCoin: Record<number, TalerErrorDetail>;
|
|
};
|
|
[TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: empty;
|
|
[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: PayMerchantInsufficientBalanceDetails;
|
|
};
|
|
[TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
|
|
insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
|
|
};
|
|
[TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
|
|
numErrors: number;
|
|
/**
|
|
* Errors, can be truncated.
|
|
*/
|
|
errors: TalerErrorDetail[];
|
|
};
|
|
}
|
|
|
|
type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
|
|
|
|
export function makeErrorDetail<C extends TalerErrorCode>(
|
|
code: C,
|
|
detail: ErrBody<C>,
|
|
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] ?? "<unknown>";
|
|
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 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);
|
|
}
|
|
}
|
|
|
|
export class TalerError<T = any> extends Error {
|
|
errorDetail: TalerErrorDetail & T;
|
|
private constructor(d: TalerErrorDetail & T) {
|
|
super(d.hint ?? `Error (code ${d.code})`);
|
|
this.errorDetail = d;
|
|
Object.setPrototypeOf(this, TalerError.prototype);
|
|
}
|
|
|
|
static fromDetail<C extends TalerErrorCode>(
|
|
code: C,
|
|
detail: ErrBody<C>,
|
|
hint?: string,
|
|
): TalerError {
|
|
if (!hint) {
|
|
hint = getDefaultHint(code);
|
|
}
|
|
const when = AbsoluteTime.now();
|
|
return new TalerError<unknown>({ code, when, 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;
|
|
}
|