diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-09-01 18:00:46 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-09-01 18:00:51 +0530 |
commit | 5056da6548d5880211abd3e1cdacd92134e40dab (patch) | |
tree | db7ede721ddd33c52dd862562b340f0782dabb22 /packages | |
parent | 5e7149f79eeb9988a7da45ecc8573c65e9680082 (diff) |
test error handling
Diffstat (limited to 'packages')
16 files changed, 278 insertions, 84 deletions
diff --git a/packages/taler-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts index fd96c3165..93999c871 100644 --- a/packages/taler-integrationtests/src/harness.ts +++ b/packages/taler-integrationtests/src/harness.ts @@ -133,6 +133,62 @@ export async function sh( }); } +function shellescape(args: string[]) { + const ret = args.map((s) => { + if (/[^A-Za-z0-9_\/:=-]/.test(s)) { + s = "'" + s.replace(/'/g, "'\\''") + "'"; + s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'"); + } + return s; + }); + return ret.join(" "); +} + +/** + * Run a shell command, return stdout. + * + * Log stderr to a log file. + */ +export async function runCommand( + t: GlobalTestState, + logName: string, + command: string, + args: string[], +): Promise<string> { + console.log("runing command", shellescape([command, ...args])); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + shell: false, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + proc.on("exit", (code, signal) => { + console.log(`child process exited (${code} / ${signal})`); + if (code != 0) { + reject(Error(`Unexpected exit code ${code} for '${command}'`)); + return; + } + const b = Buffer.concat(stdoutChunks).toString("utf-8"); + resolve(b); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + export class ProcessWrapper { private waitPromise: Promise<WaitResult>; constructor(public proc: ChildProcess) { @@ -298,7 +354,7 @@ export class GlobalTestState { } } - assertDeepEqual(actual: any, expected: any): asserts actual is any { + assertDeepEqual<T>(actual: any, expected: T): asserts actual is T { deepStrictEqual(actual, expected); } @@ -349,7 +405,9 @@ export class GlobalTestState { args: string[], logName: string, ): ProcessWrapper { - console.log(`spawning process ${command} with arguments ${args})`); + console.log( + `spawning process (${logName}): ${shellescape([command, ...args])}`, + ); const proc = spawn(command, args, { stdio: ["inherit", "pipe", "pipe"], }); @@ -1028,8 +1086,8 @@ export class ExchangeService implements ExchangeServiceInterface { await sh( this.globalState, "exchange-wire", - `taler-exchange-wire ${this.timetravelArg} -c "${this.configFilename}"` - ) + `taler-exchange-wire ${this.timetravelArg} -c "${this.configFilename}"`, + ); this.exchangeWirewatchProc = this.globalState.spawnService( "taler-exchange-wirewatch", @@ -1403,6 +1461,14 @@ export class WalletCli { fs.unlinkSync(this.dbfile); } + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + async apiRequest( request: string, payload: unknown, @@ -1420,13 +1486,19 @@ export class WalletCli { return JSON.parse(resp) as CoreApiResponse; } - async runUntilDone(): Promise<void> { - await sh( + async runUntilDone(args: { maxRetries?: number } = {}): Promise<void> { + await runCommand( this.globalTestState, `wallet-${this.name}`, - `taler-wallet-cli ${this.timetravelArg ?? ""} --no-throttle --wallet-db ${ - this.dbfile - } run-until-done`, + "taler-wallet-cli", + [ + "--no-throttle", + ...this.timetravelArgArr, + "--wallet-db", + this.dbfile, + "run-until-done", + ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []), + ], ); } diff --git a/packages/taler-integrationtests/src/helpers.ts b/packages/taler-integrationtests/src/helpers.ts index 61b015190..515ae54bd 100644 --- a/packages/taler-integrationtests/src/helpers.ts +++ b/packages/taler-integrationtests/src/helpers.ts @@ -221,7 +221,7 @@ export async function createFaultInjectedMerchantTestkudosEnvironment( /** * Withdraw balance. */ -export async function withdrawViaBank( +export async function startWithdrawViaBank( t: GlobalTestState, p: { wallet: WalletCli; @@ -255,6 +255,26 @@ export async function withdrawViaBank( talerWithdrawUri: wop.taler_withdraw_uri, }); t.assertTrue(r2.type === "response"); +} + + +/** + * Withdraw balance. + */ +export async function withdrawViaBank( + t: GlobalTestState, + p: { + wallet: WalletCli; + bank: BankService; + exchange: ExchangeService; + amount: AmountString; + }, +): Promise<void> { + + const { wallet } = p; + + await startWithdrawViaBank(t, p); + await wallet.runUntilDone(); // Check balance diff --git a/packages/taler-integrationtests/src/test-exchange-management.ts b/packages/taler-integrationtests/src/test-exchange-management.ts index 4ca86b341..7da260978 100644 --- a/packages/taler-integrationtests/src/test-exchange-management.ts +++ b/packages/taler-integrationtests/src/test-exchange-management.ts @@ -177,7 +177,7 @@ runTest(async (t: GlobalTestState) => { // Response is malformed, since it didn't even contain a version code // in a format the wallet can understand. t.assertTrue( - err1.operationError.talerErrorCode === + err1.operationError.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, ); @@ -214,7 +214,7 @@ runTest(async (t: GlobalTestState) => { }); t.assertTrue( - err2.operationError.talerErrorCode === + err2.operationError.code === TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, ); diff --git a/packages/taler-integrationtests/src/test-payment-claim.ts b/packages/taler-integrationtests/src/test-payment-claim.ts index c93057efd..3755394fd 100644 --- a/packages/taler-integrationtests/src/test-payment-claim.ts +++ b/packages/taler-integrationtests/src/test-payment-claim.ts @@ -102,7 +102,7 @@ runTest(async (t: GlobalTestState) => { }); t.assertTrue( - err.operationError.talerErrorCode === + err.operationError.code === TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED, ); diff --git a/packages/taler-integrationtests/src/test-timetravel.ts b/packages/taler-integrationtests/src/test-timetravel.ts index acc770d5d..086606b90 100644 --- a/packages/taler-integrationtests/src/test-timetravel.ts +++ b/packages/taler-integrationtests/src/test-timetravel.ts @@ -17,9 +17,18 @@ /** * Imports. */ -import { runTest, GlobalTestState, MerchantPrivateApi, WalletCli } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { PreparePayResultType, durationMin, Duration } from "taler-wallet-core"; +import { + runTest, + GlobalTestState, + MerchantPrivateApi, + WalletCli, +} from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + startWithdrawViaBank, +} from "./helpers"; +import { PreparePayResultType, durationMin, Duration, TransactionType } from "taler-wallet-core"; /** * Basic time travel test. @@ -36,7 +45,7 @@ runTest(async (t: GlobalTestState) => { // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); // Travel 400 days into the future, // as the deposit expiration is two years @@ -56,9 +65,28 @@ runTest(async (t: GlobalTestState) => { await merchant.pingUntilAvailable(); // This should fail, as the wallet didn't time travel yet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + await startWithdrawViaBank(t, { + wallet, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + // Check that transactions are correct for the failed withdrawal + { + await wallet.runUntilDone({ maxRetries: 5 }); + const transactions = await wallet.getTransactions(); + console.log(transactions); + const types = transactions.transactions.map((x) => x.type); + t.assertDeepEqual(types, ["withdrawal", "withdrawal"]); + const wtrans = transactions.transactions[0]; + t.assertTrue(wtrans.type === TransactionType.Withdrawal); + t.assertTrue(wtrans.pending); + } + + // Now we also let the wallet time travel - const bal = await wallet.getBalances(); + wallet.setTimetravel(timetravelDuration); - console.log(bal); + await wallet.runUntilDone({ maxRetries: 5 }); }); diff --git a/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts b/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts index 3c1e62924..dd848b93d 100644 --- a/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts +++ b/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts @@ -59,7 +59,7 @@ runTest(async (t: GlobalTestState) => { }); t.assertTrue(r2.type === "error"); t.assertTrue( - r2.error.talerErrorCode === + r2.error.code === TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, ); diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 94f01ba80..a19b8a8fc 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -262,9 +262,13 @@ walletCli .subcommand("finishPendingOpt", "run-until-done", { help: "Run until no more work is left.", }) + .maybeOption("maxRetries", ["--max-retries"], clk.INT) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.runUntilDoneAndStop(); + await wallet.runUntilDone({ + maxRetries: args.finishPendingOpt.maxRetries, + }); + wallet.stop(); }); }); diff --git a/packages/taler-wallet-core/src/TalerErrorCode.ts b/packages/taler-wallet-core/src/TalerErrorCode.ts index 7285a0fbe..a4c4b5a62 100644 --- a/packages/taler-wallet-core/src/TalerErrorCode.ts +++ b/packages/taler-wallet-core/src/TalerErrorCode.ts @@ -970,6 +970,13 @@ export enum TalerErrorCode { REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1516, /** + * The exchange failed to lookup information for the refund from its database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + REFUND_DATABASE_LOOKUP_ERROR = 1517, + + /** * The wire format specified in the "sender_account_details" is not understood or not supported by this 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). @@ -1572,6 +1579,20 @@ export enum TalerErrorCode { FORGET_PATH_NOT_FORGETTABLE = 2182, /** + * The merchant backend cannot forget part of an order because it failed to start the database transaction. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + FORGET_ORDER_DB_START_ERROR = 2183, + + /** + * The merchant backend cannot forget part of an order because it failed to commit the database transaction. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + FORGET_ORDER_DB_COMMIT_ERROR = 2184, + + /** * Integer overflow with specified timestamp argument detected. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). @@ -1992,6 +2013,13 @@ export enum TalerErrorCode { ORDERS_ALREADY_CLAIMED = 2521, /** + * The merchant backend couldn't find a product with the specified id. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + GET_PRODUCTS_NOT_FOUND = 2549, + + /** * The merchant backend failed to lookup the products. * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). @@ -2983,7 +3011,7 @@ export enum TalerErrorCode { * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). * (A value of 0 indicates that the error is generated client-side). */ - SYNC_DB_FETCH_ERROR = 6000, + SYNC_DB_HARD_FETCH_ERROR = 6000, /** * The sync service failed find the record in its database. @@ -3028,11 +3056,11 @@ export enum TalerErrorCode { SYNC_INVALID_SIGNATURE = 6007, /** - * The "Content-length" field for the upload is either not a number, or too big, or missing. + * The "Content-length" field for the upload is either not a number, or too big. * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). * (A value of 0 indicates that the error is generated client-side). */ - SYNC_BAD_CONTENT_LENGTH = 6008, + SYNC_MALFORMED_CONTENT_LENGTH = 6008, /** * The "Content-length" field for the upload is too big based on the server's terms of service. @@ -3112,6 +3140,27 @@ export enum TalerErrorCode { SYNC_PREVIOUS_BACKUP_UNKNOWN = 6019, /** + * The sync service had a serialization failure when accessing its database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_DB_SOFT_FETCH_ERROR = 6020, + + /** + * The sync service first found information, and then later not. This could happen if a backup was garbage collected just when it was being accessed. Trying again may give a different answer. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_DB_INCONSISTENT_FETCH_ERROR = 6021, + + /** + * The "Content-length" field for the upload is missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_MISSING_CONTENT_LENGTH = 6022, + + /** * The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange. * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). * (A value of 0 indicates that the error is generated client-side). @@ -3217,6 +3266,13 @@ export enum TalerErrorCode { WALLET_ORDER_ALREADY_CLAIMED = 7014, /** + * A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015, + + /** * End of error code range. * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). * (A value of 0 indicates that the error is generated client-side). diff --git a/packages/taler-wallet-core/src/operations/errors.ts b/packages/taler-wallet-core/src/operations/errors.ts index 76f640344..6d9f44e03 100644 --- a/packages/taler-wallet-core/src/operations/errors.ts +++ b/packages/taler-wallet-core/src/operations/errors.ts @@ -66,8 +66,8 @@ export function makeErrorDetails( details: Record<string, unknown>, ): OperationErrorDetails { return { - talerErrorCode: ec, - talerErrorHint: `Error: ${TalerErrorCode[ec]}`, + code: ec, + hint: `Error: ${TalerErrorCode[ec]}`, details: details, message, }; diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index acad5e634..881961627 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -262,6 +262,7 @@ async function gatherWithdrawalPending( source: wsr.source, withdrawalGroupId: wsr.withdrawalGroupId, lastError: wsr.lastError, + retryInfo: wsr.retryInfo, }); }); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index d869ed770..3115b9506 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -165,6 +165,7 @@ export async function getTransactions( TransactionType.Withdrawal, wsr.withdrawalGroupId, ), + ...(wsr.lastError ? { error: wsr.lastError} : {}), }); } break; diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 270735fcb..3977ba121 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -59,6 +59,7 @@ import { import { readSuccessResponseJsonOrThrow } from "../util/http"; import { URL } from "../util/url"; import { TalerErrorCode } from "../TalerErrorCode"; +import { encodeCrock } from "../crypto/talerCrypto"; const logger = new Logger("withdraw.ts"); @@ -558,9 +559,6 @@ async function incrementWithdrawalRetry( if (!wsr) { return; } - if (!wsr.retryInfo) { - return; - } wsr.retryInfo.retryCounter++; updateRetryInfoTimeout(wsr.retryInfo); wsr.lastError = err; @@ -647,12 +645,13 @@ async function processWithdrawGroupImpl( let numFinished = 0; let finishedForFirstTime = false; + let errorsPerCoin: Record<number, OperationErrorDetails> = {}; await ws.db.runWithWriteTransaction( [Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets], async (tx) => { - const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); - if (!ws) { + const wg = await tx.get(Stores.withdrawalGroups, withdrawalGroupId); + if (!wg) { return; } @@ -662,22 +661,29 @@ async function processWithdrawGroupImpl( if (x.withdrawalDone) { numFinished++; } + if (x.lastError) { + errorsPerCoin[x.coinIdx] = x.lastError; + } }); - - if (ws.timestampFinish === undefined && numFinished == numTotalCoins) { + logger.trace(`now withdrawn ${numFinished} of ${numTotalCoins} coins`); + if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { finishedForFirstTime = true; - ws.timestampFinish = getTimestampNow(); - ws.lastError = undefined; - ws.retryInfo = initRetryInfo(false); + wg.timestampFinish = getTimestampNow(); + wg.lastError = undefined; + wg.retryInfo = initRetryInfo(false); } - await tx.put(Stores.withdrawalGroups, ws); + + await tx.put(Stores.withdrawalGroups, wg); }, ); if (numFinished != numTotalCoins) { - // FIXME: aggregate individual problems into the big error message here. - throw Error( + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE, `withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`, + { + errorsPerCoin, + }, ); } diff --git a/packages/taler-wallet-core/src/types/pending.ts b/packages/taler-wallet-core/src/types/pending.ts index 85f7585c5..67d243a37 100644 --- a/packages/taler-wallet-core/src/types/pending.ts +++ b/packages/taler-wallet-core/src/types/pending.ts @@ -210,6 +210,7 @@ export interface PendingWithdrawOperation { type: PendingOperationType.Withdraw; source: WithdrawalSource; lastError: OperationErrorDetails | undefined; + retryInfo: RetryInfo; withdrawalGroupId: string; numCoinsWithdrawn: number; numCoinsTotal: number; @@ -229,6 +230,12 @@ export interface PendingOperationInfoCommon { * as opposed to some regular scheduled operation or a permanent failure. */ givesLifeness: boolean; + + /** + * Retry info, not available on all pending operations. + * If it is available, it must have the same name. + */ + retryInfo?: RetryInfo; } /** diff --git a/packages/taler-wallet-core/src/types/transactions.ts b/packages/taler-wallet-core/src/types/transactions.ts index 061ce28f4..400439548 100644 --- a/packages/taler-wallet-core/src/types/transactions.ts +++ b/packages/taler-wallet-core/src/types/transactions.ts @@ -42,6 +42,7 @@ import { codecForList, codecForAny, } from "../util/codec"; +import { OperationErrorDetails } from "./walletTypes"; export interface TransactionsRequest { /** @@ -63,24 +64,6 @@ export interface TransactionsResponse { transactions: Transaction[]; } -export interface TransactionError { - /** - * TALER_EC_* unique error code. - * The action(s) offered and message displayed on the transaction item depend on this code. - */ - ec: number; - - /** - * English-only error hint, if available. - */ - hint?: string; - - /** - * Error details specific to "ec", if applicable/available - */ - details?: any; -} - export interface TransactionCommon { // opaque unique ID for the transaction, used as a starting point for paginating queries // and for invoking actions on the transaction (e.g. deleting/hiding it from the history) @@ -103,7 +86,7 @@ export interface TransactionCommon { // Amount added or removed from the wallet's balance (including all fees and other costs) amountEffective: AmountString; - error?: TransactionError; + error?: OperationErrorDetails; } export type Transaction = diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index 2cf3c7fbc..eb7d878fa 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -51,7 +51,6 @@ import { buildCodecForUnion, } from "../util/codec"; import { AmountString, codecForContractTerms, ContractTerms } from "./talerTypes"; -import { TransactionError, OrderShortInfo, codecForOrderShortInfo } from "./transactions"; /** * Response for the create reserve request to the wallet. @@ -215,7 +214,7 @@ export interface ConfirmPayResultDone { export interface ConfirmPayResultPending { type: ConfirmPayResultType.Pending; - lastError: TransactionError; + lastError: OperationErrorDetails; } export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 352cb29ef..845c6d71d 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -299,10 +299,15 @@ export class Wallet { * liveness left. The wallet will be in a stopped state when this function * returns without resolving to an exception. */ - public async runUntilDone(): Promise<void> { + public async runUntilDone( + req: { + maxRetries?: number; + } = {}, + ): Promise<void> { let done = false; const p = new Promise((resolve, reject) => { - // Run this asynchronously + // Monitor for conditions that means we're done or we + // should quit with an error (due to exceeded retries). this.addNotificationListener((n) => { if (done) { return; @@ -315,7 +320,29 @@ export class Wallet { logger.trace("no liveness-giving operations left"); resolve(); } + const maxRetries = req.maxRetries; + if (!maxRetries) { + return; + } + this.getPendingOperations({ onlyDue: false }) + .then((pending) => { + for (const p of pending.pendingOperations) { + if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) { + console.warn( + `stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`, + ); + this.stop(); + done = true; + resolve(); + } + } + }) + .catch((e) => { + logger.error(e); + reject(e); + }); }); + // Run this asynchronously this.runRetryLoop().catch((e) => { logger.error("exception in wallet retry loop"); reject(e); @@ -324,16 +351,6 @@ export class Wallet { await p; } - /** - * Run the wallet until there are no more pending operations that give - * liveness left. The wallet will be in a stopped state when this function - * returns without resolving to an exception. - */ - public async runUntilDoneAndStop(): Promise<void> { - await this.runUntilDone(); - logger.trace("stopping after liveness-giving operations done"); - this.stop(); - } /** * Process pending operations and wait for scheduled operations in @@ -392,7 +409,7 @@ export class Wallet { if (e instanceof OperationFailedAndReportedError) { logger.warn("operation processed resulted in reported error"); } else { - console.error("Uncaught exception", e); + logger.error("Uncaught exception", e); this.ws.notify({ type: NotificationType.InternalError, message: "uncaught exception", @@ -902,10 +919,13 @@ export class Wallet { return getTransactions(this.ws, request); } - async withdrawTestBalance( - req: WithdrawTestBalanceRequest, - ): Promise<void> { - await withdrawTestBalance(this.ws, req.amount, req.bankBaseUrl, req.exchangeBaseUrl); + async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise<void> { + await withdrawTestBalance( + this.ws, + req.amount, + req.bankBaseUrl, + req.exchangeBaseUrl, + ); } async runIntegrationtest(args: IntegrationTestArgs): Promise<void> { @@ -940,12 +960,12 @@ export class Wallet { case "runIntegrationTest": { const req = codecForIntegrationTestArgs().decode(payload); await this.runIntegrationtest(req); - return {} + return {}; } case "testPay": { const req = codecForTestPayArgs().decode(payload); await this.testPay(req); - return {} + return {}; } case "getTransactions": { const req = codecForTransactionsRequest().decode(payload); @@ -988,10 +1008,7 @@ export class Wallet { } case "setExchangeTosAccepted": { const req = codecForAcceptExchangeTosRequest().decode(payload); - await this.acceptExchangeTermsOfService( - req.exchangeBaseUrl, - req.etag, - ); + await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag); return {}; } case "applyRefund": { |