From d41ae5eb97a5264b1d61321354eac049ca317c97 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 15 Jun 2021 18:52:43 +0200 Subject: separate wallet state from wallet client --- packages/taler-util/src/walletTypes.ts | 14 + packages/taler-wallet-android/src/index.ts | 40 +- packages/taler-wallet-cli/src/index.ts | 362 ++-- .../src/integrationtests/harness.ts | 5 +- .../src/integrationtests/test-payment-fault.ts | 2 +- packages/taler-wallet-core/src/db.ts | 2 +- packages/taler-wallet-core/src/headless/helpers.ts | 7 +- packages/taler-wallet-core/src/index.ts | 34 +- .../src/operations/backup/index.ts | 16 +- .../taler-wallet-core/src/operations/exchanges.ts | 7 +- .../taler-wallet-core/src/operations/refresh.ts | 5 +- packages/taler-wallet-core/src/operations/state.ts | 26 +- .../taler-wallet-core/src/operations/testing.ts | 88 +- .../src/operations/transactions.ts | 2 +- .../src/util/coinSelection.test.ts | 2 +- packages/taler-wallet-core/src/util/http.ts | 5 +- packages/taler-wallet-core/src/wallet.ts | 1929 ++++++++++---------- .../taler-wallet-webextension/src/wxBackend.ts | 16 +- 18 files changed, 1284 insertions(+), 1278 deletions(-) (limited to 'packages') diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 6a0c57139..4d49db029 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -622,11 +622,13 @@ export const codecForIntegrationTestArgs = (): Codec => export interface AddExchangeRequest { exchangeBaseUrl: string; + forceUpdate?: boolean; } export const codecForAddExchangeRequest = (): Codec => buildCodecForObject() .property("exchangeBaseUrl", codecForString()) + .property("forceUpdate", codecOptional(codecForBoolean())) .build("AddExchangeRequest"); export interface ForceExchangeUpdateRequest { @@ -962,3 +964,15 @@ export const codecForRetryTransactionRequest = (): Codec() .property("transactionId", codecForString()) .build("RetryTransactionRequest"); + +export interface SetWalletDeviceIdRequest { + /** + * New wallet device ID to set. + */ + walletDeviceId: string; +} + +export const codecForSetWalletDeviceIdRequest = (): Codec => + buildCodecForObject() + .property("walletDeviceId", codecForString()) + .build("SetWalletDeviceIdRequest"); diff --git a/packages/taler-wallet-android/src/index.ts b/packages/taler-wallet-android/src/index.ts index 0be45ae7c..6f6439fb9 100644 --- a/packages/taler-wallet-android/src/index.ts +++ b/packages/taler-wallet-android/src/index.ts @@ -18,7 +18,6 @@ * Imports. */ import { - Wallet, getDefaultNodeWallet, DefaultNodeWalletArgs, NodeHttpLib, @@ -33,7 +32,10 @@ import { Headers, WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION, + runRetryLoop, + handleCoreApiRequest, } from "@gnu-taler/taler-wallet-core"; +import { InternalWalletState } from "@gnu-taler/taler-wallet-core/lib/operations/state"; import fs from "fs"; import { WalletNotification } from "../../taler-wallet-core/node_modules/@gnu-taler/taler-util/lib/notifications.js"; @@ -154,8 +156,8 @@ function sendAkonoMessage(ev: CoreApiEnvelope): void { class AndroidWalletMessageHandler { walletArgs: DefaultNodeWalletArgs | undefined; - maybeWallet: Wallet | undefined; - wp = openPromise(); + maybeWallet: InternalWalletState | undefined; + wp = openPromise(); httpLib = new NodeHttpLib(); /** @@ -174,6 +176,17 @@ class AndroidWalletMessageHandler { result, }; }; + + const reinit = async () => { + const w = await getDefaultNodeWallet(this.walletArgs); + this.maybeWallet = w; + await handleCoreApiRequest(w, "initWallet", "akono-init", {}); + runRetryLoop(w).catch((e) => { + console.error("Error during wallet retry loop", e); + }); + this.wp.resolve(w); + }; + switch (operation) { case "init": { this.walletArgs = { @@ -183,12 +196,7 @@ class AndroidWalletMessageHandler { persistentStoragePath: args.persistentStoragePath, httpLib: this.httpLib, }; - const w = await getDefaultNodeWallet(this.walletArgs); - this.maybeWallet = w; - w.runRetryLoop().catch((e) => { - console.error("Error during wallet retry loop", e); - }); - this.wp.resolve(w); + await reinit(); return wrapResponse({ supported_protocol_versions: { exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, @@ -196,9 +204,6 @@ class AndroidWalletMessageHandler { }, }); } - case "getHistory": { - return wrapResponse({ history: [] }); - } case "startTunnel": { // this.httpLib.useNfcTunnel = true; throw Error("not implemented"); @@ -225,19 +230,14 @@ class AndroidWalletMessageHandler { } const wallet = await this.wp.promise; wallet.stop(); - this.wp = openPromise(); + this.wp = openPromise(); this.maybeWallet = undefined; - const w = await getDefaultNodeWallet(this.walletArgs); - this.maybeWallet = w; - w.runRetryLoop().catch((e) => { - console.error("Error during wallet retry loop", e); - }); - this.wp.resolve(w); + await reinit(); return wrapResponse({}); } default: { const wallet = await this.wp.promise; - return await wallet.handleCoreApiRequest(operation, id, args); + return await handleCoreApiRequest(wallet, operation, id, args); } } } diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index c98ece941..63b969f18 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -33,9 +33,9 @@ import { codecForList, codecForString, Logger, + WithdrawalType, } from "@gnu-taler/taler-util"; import { - Wallet, NodeHttpLib, getDefaultNodeWallet, OperationFailedAndReportedError, @@ -45,7 +45,14 @@ import { NodeThreadCryptoWorkerFactory, CryptoApi, walletCoreDebugFlags, + WalletCoreApiClient, + WalletApiOperation, + handleCoreApiRequest, + runPending, + runUntilDone, + getClientFromWalletState, } from "@gnu-taler/taler-wallet-core"; +import { InternalWalletState } from "@gnu-taler/taler-wallet-core/src/operations/state"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -63,11 +70,13 @@ function assertUnreachable(x: never): never { } async function doPay( - wallet: Wallet, + wallet: WalletCoreApiClient, payUrl: string, options: { alwaysYes: boolean } = { alwaysYes: true }, ): Promise { - const result = await wallet.preparePayForUri(payUrl); + const result = await wallet.call(WalletApiOperation.PreparePayForUri, { + talerPayUri: payUrl, + }); if (result.status === PreparePayResultType.InsufficientBalance) { console.log("contract", result.contractTerms); console.error("insufficient balance"); @@ -111,7 +120,9 @@ async function doPay( } if (pay) { - await wallet.confirmPay(result.proposalId, undefined); + await wallet.call(WalletApiOperation.ConfirmPay, { + proposalId: result.proposalId, + }); } else { console.log("not paying"); } @@ -161,7 +172,10 @@ type WalletCliArgsType = clk.GetArgType; async function withWallet( walletCliArgs: WalletCliArgsType, - f: (w: Wallet) => Promise, + f: (w: { + client: WalletCoreApiClient; + ws: InternalWalletState; + }) => Promise, ): Promise { const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; const myHttpLib = new NodeHttpLib(); @@ -174,8 +188,11 @@ async function withWallet( }); applyVerbose(walletCliArgs.wallet.verbose); try { - await wallet.fillDefaults(); - const ret = await f(wallet); + const w = { + ws: wallet, + client: await getClientFromWalletState(wallet), + }; + const ret = await f(w); return ret; } catch (e) { if ( @@ -204,7 +221,10 @@ walletCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - const balance = await wallet.getBalances(); + const balance = await wallet.client.call( + WalletApiOperation.GetBalances, + {}, + ); console.log(JSON.stringify(balance, undefined, 2)); }); }); @@ -222,7 +242,8 @@ walletCli console.error("Invalid JSON"); process.exit(1); } - const resp = await wallet.handleCoreApiRequest( + const resp = await handleCoreApiRequest( + wallet.ws, args.api.operation, "reqid-1", requestJson, @@ -235,7 +256,10 @@ walletCli .subcommand("", "pending", { help: "Show pending operations." }) .action(async (args) => { await withWallet(args, async (wallet) => { - const pending = await wallet.getPendingOperations(); + const pending = await wallet.client.call( + WalletApiOperation.GetPendingOperations, + {}, + ); console.log(JSON.stringify(pending, undefined, 2)); }); }); @@ -246,10 +270,13 @@ walletCli .maybeOption("search", ["--search"], clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const pending = await wallet.getTransactions({ - currency: args.transactions.currency, - search: args.transactions.search, - }); + const pending = await wallet.client.call( + WalletApiOperation.GetTransactions, + { + currency: args.transactions.currency, + search: args.transactions.search, + }, + ); console.log(JSON.stringify(pending, undefined, 2)); }); }); @@ -267,7 +294,20 @@ walletCli .flag("forceNow", ["-f", "--force-now"]) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.runPending(args.runPendingOpt.forceNow); + await runPending(wallet.ws, args.runPendingOpt.forceNow); + }); + }); + +walletCli + .subcommand("retryTransaction", "retry-transaction", { + help: "Retry a transaction.", + }) + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.client.call(WalletApiOperation.RetryTransaction, { + transactionId: args.retryTransaction.transactionId, + }); }); }); @@ -278,10 +318,10 @@ walletCli .maybeOption("maxRetries", ["--max-retries"], clk.INT) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.runUntilDone({ + await runUntilDone(wallet.ws, { maxRetries: args.finishPendingOpt.maxRetries, }); - wallet.stop(); + wallet.ws.stop(); }); }); @@ -294,7 +334,7 @@ walletCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.deleteTransaction({ + await wallet.client.call(WalletApiOperation.DeleteTransaction, { transactionId: args.deleteTransaction.transactionId, }); }); @@ -312,29 +352,51 @@ walletCli const uriType = classifyTalerUri(uri); switch (uriType) { case TalerUriType.TalerPay: - await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes }); + await doPay(wallet.client, uri, { + alwaysYes: args.handleUri.autoYes, + }); break; case TalerUriType.TalerTip: { - const res = await wallet.prepareTip(uri); + const res = await wallet.client.call( + WalletApiOperation.PrepareTip, + { + talerTipUri: uri, + }, + ); console.log("tip status", res); - await wallet.acceptTip(res.walletTipId); + await wallet.client.call(WalletApiOperation.AcceptTip, { + walletTipId: res.walletTipId, + }); } break; case TalerUriType.TalerRefund: - await wallet.applyRefund(uri); + await wallet.client.call(WalletApiOperation.ApplyRefund, { + talerRefundUri: uri, + }); break; case TalerUriType.TalerWithdraw: { - const withdrawInfo = await wallet.getWithdrawalDetailsForUri(uri); + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: uri, + }, + ); + console.log("withdrawInfo", withdrawInfo); const selectedExchange = withdrawInfo.defaultExchangeBaseUrl; if (!selectedExchange) { console.error("no suggested exchange!"); process.exit(1); return; } - const res = await wallet.acceptWithdrawal(uri, selectedExchange); - await wallet.processReserve(res.reservePub); + const res = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: selectedExchange, + talerWithdrawUri: uri, + }, + ); } break; default: @@ -356,7 +418,10 @@ exchangesCli .action(async (args) => { console.log("Listing exchanges ..."); await withWallet(args, async (wallet) => { - const exchanges = await wallet.getExchanges(); + const exchanges = await wallet.client.call( + WalletApiOperation.ListExchanges, + {}, + ); console.log(JSON.stringify(exchanges, undefined, 2)); }); }); @@ -371,10 +436,10 @@ exchangesCli .flag("force", ["-f", "--force"]) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.updateExchangeFromUrl( - args.exchangesUpdateCmd.url, - args.exchangesUpdateCmd.force, - ); + await wallet.client.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: args.exchangesUpdateCmd.url, + forceUpdate: args.exchangesUpdateCmd.force, + }); }); }); @@ -387,7 +452,9 @@ exchangesCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.updateExchangeFromUrl(args.exchangesAddCmd.url); + await wallet.client.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: args.exchangesAddCmd.url, + }); }); }); @@ -403,10 +470,10 @@ exchangesCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.acceptExchangeTermsOfService( - args.exchangesAcceptTosCmd.url, - args.exchangesAcceptTosCmd.etag, - ); + await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, { + etag: args.exchangesAcceptTosCmd.etag, + exchangeBaseUrl: args.exchangesAcceptTosCmd.url, + }); }); }); @@ -419,7 +486,12 @@ exchangesCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - const tosResult = await wallet.getExchangeTos(args.exchangesTosCmd.url); + const tosResult = await wallet.client.call( + WalletApiOperation.GetExchangeTos, + { + exchangeBaseUrl: args.exchangesTosCmd.url, + }, + ); console.log(JSON.stringify(tosResult, undefined, 2)); }); }); @@ -435,65 +507,44 @@ backupCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - const backup = await wallet.setDeviceId(args.setDeviceId.deviceId); - console.log(JSON.stringify(backup, undefined, 2)); + await wallet.client.call(WalletApiOperation.SetWalletDeviceId, { + walletDeviceId: args.setDeviceId.deviceId, + }); }); }); backupCli.subcommand("exportPlain", "export-plain").action(async (args) => { await withWallet(args, async (wallet) => { - const backup = await wallet.exportBackupPlain(); + const backup = await wallet.client.call( + WalletApiOperation.ExportBackupPlain, + {}, + ); console.log(JSON.stringify(backup, undefined, 2)); }); }); -backupCli - .subcommand("export", "export") - .requiredArgument("filename", clk.STRING, { - help: "backup filename", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const backup = await wallet.exportBackupEncrypted(); - fs.writeFileSync(args.export.filename, backup); - }); - }); - -backupCli - .subcommand("import", "import") - .requiredArgument("filename", clk.STRING, { - help: "backup filename", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const backupEncBlob = fs.readFileSync(args.import.filename); - await wallet.importBackupEncrypted(backupEncBlob); - }); - }); - -backupCli.subcommand("importPlain", "import-plain").action(async (args) => { - await withWallet(args, async (wallet) => { - const data = JSON.parse(await read(process.stdin)); - await wallet.importBackupPlain(data); - }); -}); - backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => { await withWallet(args, async (wallet) => { - const recoveryJson = await wallet.getBackupRecovery(); + const recoveryJson = await wallet.client.call( + WalletApiOperation.ExportBackupRecovery, + {}, + ); console.log(JSON.stringify(recoveryJson, undefined, 2)); }); }); backupCli.subcommand("run", "run").action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.runBackupCycle(); + await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); }); }); backupCli.subcommand("status", "status").action(async (args) => { await withWallet(args, async (wallet) => { - const status = await wallet.getBackupStatus(); + const status = await wallet.client.call( + WalletApiOperation.GetBackupInfo, + {}, + ); console.log(JSON.stringify(status, undefined, 2)); }); }); @@ -518,7 +569,7 @@ backupCli throw Error("invalid recovery strategy"); } } - await wallet.loadBackupRecovery({ + await wallet.client.call(WalletApiOperation.ImportBackupRecovery, { recovery: data, strategy, }); @@ -531,7 +582,7 @@ backupCli .flag("activate", ["--activate"]) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.addBackupProvider({ + await wallet.client.call(WalletApiOperation.AddBackupProvider, { backupProviderBaseUrl: args.addProvider.url, activate: args.addProvider.activate, }); @@ -548,12 +599,15 @@ depositCli .requiredArgument("targetPayto", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const resp = await wallet.createDepositGroup({ - amount: args.createDepositArgs.amount, - depositPaytoUri: args.createDepositArgs.targetPayto, - }); + const resp = await wallet.client.call( + WalletApiOperation.CreateDepositGroup, + { + amount: args.createDepositArgs.amount, + depositPaytoUri: args.createDepositArgs.targetPayto, + }, + ); console.log(`Created deposit ${resp.depositGroupId}`); - await wallet.runPending(); + await runPending(wallet.ws); }); }); @@ -562,9 +616,12 @@ depositCli .requiredArgument("depositGroupId", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const resp = await wallet.trackDepositGroup({ - depositGroupId: args.trackDepositArgs.depositGroupId, - }); + const resp = await wallet.client.call( + WalletApiOperation.TrackDepositGroup, + { + depositGroupId: args.trackDepositArgs.depositGroupId, + }, + ); console.log(JSON.stringify(resp, undefined, 2)); }); }); @@ -582,9 +639,12 @@ advancedCli .requiredArgument("amount", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const details = await wallet.getWithdrawalDetailsForAmount( - args.manualWithdrawalDetails.exchange, - Amounts.parseOrThrow(args.manualWithdrawalDetails.amount), + const details = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: args.manualWithdrawalDetails.amount, + exchangeBaseUrl: args.manualWithdrawalDetails.exchange, + }, ); console.log(JSON.stringify(details, undefined, 2)); }); @@ -611,23 +671,33 @@ advancedCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - const { exchange, exchangeDetails } = await wallet.updateExchangeFromUrl( - args.withdrawManually.exchange, + const exchangeBaseUrl = args.withdrawManually.exchange; + const amount = args.withdrawManually.amount; + const d = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: args.withdrawManually.amount, + exchangeBaseUrl: exchangeBaseUrl, + }, ); - const acct = exchangeDetails.wireInfo.accounts[0]; + const acct = d.paytoUris[0]; if (!acct) { console.log("exchange has no accounts"); return; } - const reserve = await wallet.acceptManualWithdrawal( - exchange.baseUrl, - Amounts.parseOrThrow(args.withdrawManually.amount), + const resp = await wallet.client.call( + WalletApiOperation.AcceptManualWithdrawal, + { + amount, + exchangeBaseUrl, + }, ); - const completePaytoUri = addPaytoQueryParams(acct.payto_uri, { + const reservePub = resp.reservePub; + const completePaytoUri = addPaytoQueryParams(acct, { amount: args.withdrawManually.amount, - message: `Taler top-up ${reserve.reservePub}`, + message: `Taler top-up ${reservePub}`, }); - console.log("Created reserve", reserve.reservePub); + console.log("Created reserve", reservePub); console.log("Payto URI", completePaytoUri); }); }); @@ -640,37 +710,14 @@ currenciesCli .subcommand("show", "show", { help: "Show currencies." }) .action(async (args) => { await withWallet(args, async (wallet) => { - const currencies = await wallet.getCurrencies(); + const currencies = await wallet.client.call( + WalletApiOperation.ListCurrencies, + {}, + ); console.log(JSON.stringify(currencies, undefined, 2)); }); }); -const reservesCli = advancedCli.subcommand("reserves", "reserves", { - help: "Manage reserves.", -}); - -reservesCli - .subcommand("list", "list", { - help: "List reserves.", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const reserves = await wallet.getReservesForExchange(); - console.log(JSON.stringify(reserves, undefined, 2)); - }); - }); - -reservesCli - .subcommand("update", "update", { - help: "Update reserve status via exchange.", - }) - .requiredArgument("reservePub", clk.STRING) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.updateReserve(args.update.reservePub); - }); - }); - advancedCli .subcommand("payPrepare", "pay-prepare", { help: "Claim an order but don't pay yet.", @@ -678,7 +725,12 @@ advancedCli .requiredArgument("url", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - const res = await wallet.preparePayForUri(args.payPrepare.url); + const res = await wallet.client.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: args.payPrepare.url, + }, + ); switch (res.status) { case PreparePayResultType.InsufficientBalance: console.log("insufficient balance"); @@ -707,10 +759,10 @@ advancedCli .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - wallet.confirmPay( - args.payConfirm.proposalId, - args.payConfirm.sessionIdOverride, - ); + await wallet.client.call(WalletApiOperation.ConfirmPay, { + proposalId: args.payConfirm.proposalId, + sessionId: args.payConfirm.sessionIdOverride, + }); }); }); @@ -721,7 +773,9 @@ advancedCli .requiredArgument("coinPub", clk.STRING) .action(async (args) => { await withWallet(args, async (wallet) => { - await wallet.refresh(args.refresh.coinPub); + await wallet.client.call(WalletApiOperation.ForceRefresh, { + coinPubList: [args.refresh.coinPub], + }); }); }); @@ -731,7 +785,10 @@ advancedCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - const coinDump = await wallet.dumpCoins(); + const coinDump = await wallet.client.call( + WalletApiOperation.DumpCoins, + {}, + ); console.log(JSON.stringify(coinDump, undefined, 2)); }); }); @@ -755,7 +812,10 @@ advancedCli process.exit(1); } for (const c of coinPubList) { - await wallet.setCoinSuspended(c, true); + await wallet.client.call(WalletApiOperation.SetCoinSuspended, { + coinPub: c, + suspended: true, + }); } }); }); @@ -777,7 +837,10 @@ advancedCli process.exit(1); } for (const c of coinPubList) { - await wallet.setCoinSuspended(c, false); + await wallet.client.call(WalletApiOperation.SetCoinSuspended, { + coinPub: c, + suspended: false, + }); } }); }); @@ -788,43 +851,18 @@ advancedCli }) .action(async (args) => { await withWallet(args, async (wallet) => { - const coins = await wallet.getCoins(); - for (const coin of coins) { - console.log(`coin ${coin.coinPub}`); - console.log(` status ${coin.status}`); - console.log(` exchange ${coin.exchangeBaseUrl}`); - console.log(` denomPubHash ${coin.denomPubHash}`); + const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {}); + for (const coin of coins.coins) { + console.log(`coin ${coin.coin_pub}`); + console.log(` exchange ${coin.exchange_base_url}`); + console.log(` denomPubHash ${coin.denom_pub_hash}`); console.log( - ` remaining amount ${Amounts.stringify(coin.currentAmount)}`, + ` remaining amount ${Amounts.stringify(coin.remaining_value)}`, ); } }); }); -advancedCli - .subcommand("updateReserve", "update-reserve", { - help: "Update reserve status.", - }) - .requiredArgument("reservePub", clk.STRING) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const r = await wallet.updateReserve(args.updateReserve.reservePub); - console.log("updated reserve:", JSON.stringify(r, undefined, 2)); - }); - }); - -advancedCli - .subcommand("updateReserve", "show-reserve", { - help: "Show the current reserve status.", - }) - .requiredArgument("reservePub", clk.STRING) - .action(async (args) => { - await withWallet(args, async (wallet) => { - const r = await wallet.getReserve(args.updateReserve.reservePub); - console.log("updated reserve:", JSON.stringify(r, undefined, 2)); - }); - }); - const testCli = walletCli.subcommand("testingArgs", "testing", { help: "Subcommands for testing GNU Taler deployments.", }); diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts index b6ea02696..b0a538a72 100644 --- a/packages/taler-wallet-cli/src/integrationtests/harness.ts +++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts @@ -1783,7 +1783,10 @@ export class WalletCli { } async forceUpdateExchange(req: ForceExchangeUpdateRequest): Promise { - const resp = await this.apiRequest("forceUpdateExchange", req); + const resp = await this.apiRequest("addExchange", { + exchangeBaseUrl: req.exchangeBaseUrl, + forceUpdate: true, + }); if (resp.type === "response") { return; } diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts index 26b8566bb..37ae0739c 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts @@ -213,4 +213,4 @@ export async function runPaymentFaultTest(t: GlobalTestState) { } runPaymentFaultTest.suites = ["wallet"]; -runPaymentFaultTest.timeoutMs = 120000; \ No newline at end of file +runPaymentFaultTest.timeoutMs = 120000; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 8f9d5757d..2d2c0615c 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -576,7 +576,7 @@ export interface ExchangeDetailsRecord { /** * Timestamp when the ToS was accepted. - * + * * Used during backup merging. */ termsOfServiceAcceptedTimestamp: Timestamp | undefined; diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts index 7b918d5d9..8125ef6b0 100644 --- a/packages/taler-wallet-core/src/headless/helpers.ts +++ b/packages/taler-wallet-core/src/headless/helpers.ts @@ -22,7 +22,6 @@ /** * Imports. */ -import { Wallet } from "../wallet"; import { MemoryBackend, BridgeIDBFactory, @@ -36,6 +35,7 @@ import { Logger } from "@gnu-taler/taler-util"; import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; import type { IDBFactory } from "@gnu-taler/idb-bridge"; import { WalletNotification } from "@gnu-taler/taler-util"; +import { InternalWalletState } from "../operations/state.js"; const logger = new Logger("headless/helpers.ts"); @@ -93,7 +93,7 @@ function makeId(length: number): string { */ export async function getDefaultNodeWallet( args: DefaultNodeWalletArgs = {}, -): Promise { +): Promise { BridgeIDBFactory.enableTracing = false; const myBackend = new MemoryBackend(); myBackend.enableTracing = false; @@ -172,7 +172,8 @@ export async function getDefaultNodeWallet( workerFactory = new SynchronousCryptoWorkerFactory(); } - const w = new Wallet(myDb, myHttpLib, workerFactory); + const w = new InternalWalletState(myDb, myHttpLib, workerFactory); + if (args.notifyHandler) { w.addNotificationListener(args.notifyHandler); } diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index 459c4c07c..24109d9b8 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -19,34 +19,34 @@ */ // Errors -export * from "./operations/errors"; +export * from "./operations/errors.js"; // Util functionality -export { URL } from "./util/url"; -export * from "./util/promiseUtils"; -export * from "./util/query"; -export * from "./util/http"; +export { URL } from "./util/url.js"; +export * from "./util/promiseUtils.js"; +export * from "./util/query.js"; +export * from "./util/http.js"; // Utils for using the wallet under node -export { NodeHttpLib } from "./headless/NodeHttpLib"; +export { NodeHttpLib } from "./headless/NodeHttpLib.js"; export { getDefaultNodeWallet, DefaultNodeWalletArgs, -} from "./headless/helpers"; +} from "./headless/helpers.js"; -export * from "./operations/versions"; +export * from "./operations/versions.js"; -export * from "./db"; +export * from "./db.js"; // Crypto and crypto workers -export * from "./crypto/workers/nodeThreadWorker"; -export { CryptoImplementation } from "./crypto/workers/cryptoImplementation"; -export type { CryptoWorker } from "./crypto/workers/cryptoWorker"; -export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi"; -export * from "./crypto/talerCrypto"; +export * from "./crypto/workers/nodeThreadWorker.js"; +export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js"; +export type { CryptoWorker } from "./crypto/workers/cryptoWorker.js"; +export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js"; +export * from "./crypto/talerCrypto.js"; -export * from "./pending-types"; +export * from "./pending-types.js"; -export * from "./util/debugFlags"; +export * from "./util/debugFlags.js"; -export { Wallet } from "./wallet"; +export * from "./wallet.js"; diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts b/packages/taler-wallet-core/src/operations/backup/index.ts index 00a76bd19..2cc056721 100644 --- a/packages/taler-wallet-core/src/operations/backup/index.ts +++ b/packages/taler-wallet-core/src/operations/backup/index.ts @@ -38,7 +38,10 @@ import { WalletBackupConfState, WALLET_BACKUP_STATE_KEY, } from "../../db.js"; -import { checkDbInvariant, checkLogicInvariant } from "../../util/invariants.js"; +import { + checkDbInvariant, + checkLogicInvariant, +} from "../../util/invariants.js"; import { bytesToString, decodeCrock, @@ -83,8 +86,15 @@ import { TalerErrorDetails, } from "@gnu-taler/taler-util"; import { CryptoApi } from "../../crypto/workers/cryptoApi.js"; -import { secretbox, secretbox_open } from "../../crypto/primitives/nacl-fast.js"; -import { checkPaymentByProposalId, confirmPay, preparePayForUri } from "../pay.js"; +import { + secretbox, + secretbox_open, +} from "../../crypto/primitives/nacl-fast.js"; +import { + checkPaymentByProposalId, + confirmPay, + preparePayForUri, +} from "../pay.js"; import { exportBackup } from "./export.js"; import { BackupCryptoPrecomputedData, importBackup } from "./import.js"; import { provideBackupState, getWalletBackupState } from "./state.js"; diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 1948f70e1..c8dfcbc17 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -60,7 +60,12 @@ import { WALLET_CACHE_BREAKER_CLIENT_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, } from "./versions.js"; -import { getExpiryTimestamp, HttpRequestLibrary, readSuccessResponseJsonOrThrow, readSuccessResponseTextOrThrow } from "../util/http.js"; +import { + getExpiryTimestamp, + HttpRequestLibrary, + readSuccessResponseJsonOrThrow, + readSuccessResponseTextOrThrow, +} from "../util/http.js"; import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js"; import { decodeCrock, encodeCrock, hash } from "../crypto/talerCrypto.js"; diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index c442a7c90..3c81362ce 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -55,7 +55,10 @@ import { URL } from "../util/url.js"; import { guardOperationException } from "./errors.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { EXCHANGE_COINS_LOCK, InternalWalletState } from "./state.js"; -import { isWithdrawableDenom, selectWithdrawalDenominations } from "./withdraw.js"; +import { + isWithdrawableDenom, + selectWithdrawalDenominations, +} from "./withdraw.js"; import { RefreshNewDenomInfo } from "../crypto/cryptoTypes.js"; import { GetReadWriteAccess } from "../util/query.js"; diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts index 66baa95a4..ee7ceb8af 100644 --- a/packages/taler-wallet-core/src/operations/state.ts +++ b/packages/taler-wallet-core/src/operations/state.ts @@ -27,8 +27,13 @@ import { WalletStoresV1 } from "../db.js"; import { PendingOperationsResponse } from "../pending-types.js"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo.js"; import { HttpRequestLibrary } from "../util/http"; -import { OpenedPromise, openPromise } from "../util/promiseUtils.js"; +import { + AsyncCondition, + OpenedPromise, + openPromise, +} from "../util/promiseUtils.js"; import { DbAccess } from "../util/query.js"; +import { TimerGroup } from "../util/timer.js"; type NotificationListener = (n: WalletNotification) => void; @@ -37,6 +42,9 @@ const logger = new Logger("state.ts"); export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; +/** + * Internal state of the wallet. + */ export class InternalWalletState { memoProcessReserve: AsyncOpMemoMap = new AsyncOpMemoMap(); memoMakePlanchet: AsyncOpMemoMap = new AsyncOpMemoMap(); @@ -47,8 +55,15 @@ export class InternalWalletState { memoProcessDeposit: AsyncOpMemoMap = new AsyncOpMemoMap(); cryptoApi: CryptoApi; + timerGroup: TimerGroup = new TimerGroup(); + latch = new AsyncCondition(); + stopped = false; + memoRunRetryLoop = new AsyncOpMemoSingle(); + listeners: NotificationListener[] = []; + initCalled: boolean = false; + /** * Promises that are waiting for a particular resource. */ @@ -85,6 +100,15 @@ export class InternalWalletState { this.listeners.push(f); } + /** + * Stop ongoing processing. + */ + stop(): void { + this.stopped = true; + this.timerGroup.stopCurrentAndFutureTimers(); + this.cryptoApi.stop(); + } + /** * Run an async function after acquiring a list of locks, identified * by string tokens. diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index b163569ae..ce3a47f36 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -33,10 +33,13 @@ import { TestPayArgs, PreparePayResultType, } from "@gnu-taler/taler-util"; -import { Wallet } from "../wallet.js"; import { createTalerWithdrawReserve } from "./reserves.js"; import { InternalWalletState } from "./state.js"; import { URL } from "../util/url.js"; +import { confirmPay, preparePayForUri } from "./pay.js"; +import { getBalances } from "./balance.js"; +import { runUntilDone } from "../wallet.js"; +import { applyRefund } from "./refund.js"; const logger = new Logger("operations/testing.ts"); @@ -261,14 +264,13 @@ interface BankWithdrawalResponse { } async function makePayment( - http: HttpRequestLibrary, - wallet: Wallet, + ws: InternalWalletState, merchant: MerchantBackendInfo, amount: string, summary: string, ): Promise<{ orderId: string }> { const orderResp = await createOrder( - http, + ws.http, merchant, amount, summary, @@ -277,7 +279,7 @@ async function makePayment( logger.trace("created order with orderId", orderResp.orderId); - let paymentStatus = await checkPayment(http, merchant, orderResp.orderId); + let paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId); logger.trace("payment status", paymentStatus); @@ -286,7 +288,7 @@ async function makePayment( throw Error("no taler://pay/ URI in payment response"); } - const preparePayResult = await wallet.preparePayForUri(talerPayUri); + const preparePayResult = await preparePayForUri(ws, talerPayUri); logger.trace("prepare pay result", preparePayResult); @@ -294,14 +296,15 @@ async function makePayment( throw Error("payment not possible"); } - const confirmPayResult = await wallet.confirmPay( + const confirmPayResult = await confirmPay( + ws, preparePayResult.proposalId, undefined, ); logger.trace("confirmPayResult", confirmPayResult); - paymentStatus = await checkPayment(http, merchant, orderResp.orderId); + paymentStatus = await checkPayment(ws.http, merchant, orderResp.orderId); logger.trace("payment status after wallet payment:", paymentStatus); @@ -315,8 +318,7 @@ async function makePayment( } export async function runIntegrationTest( - http: HttpRequestLibrary, - wallet: Wallet, + ws: InternalWalletState, args: IntegrationTestArgs, ): Promise { logger.info("running test with arguments", args); @@ -325,15 +327,16 @@ export async function runIntegrationTest( const currency = parsedSpendAmount.currency; logger.info("withdrawing test balance"); - await wallet.withdrawTestBalance({ - amount: args.amountToWithdraw, - bankBaseUrl: args.bankBaseUrl, - exchangeBaseUrl: args.exchangeBaseUrl, - }); - await wallet.runUntilDone(); + await withdrawTestBalance( + ws, + args.amountToWithdraw, + args.bankBaseUrl, + args.exchangeBaseUrl, + ); + await runUntilDone(ws); logger.info("done withdrawing test balance"); - const balance = await wallet.getBalances(); + const balance = await getBalances(ws); logger.trace(JSON.stringify(balance, null, 2)); @@ -342,16 +345,10 @@ export async function runIntegrationTest( authToken: args.merchantAuthToken, }; - await makePayment( - http, - wallet, - myMerchant, - args.amountToSpend, - "hello world", - ); + await makePayment(ws, myMerchant, args.amountToSpend, "hello world"); // Wait until the refresh is done - await wallet.runUntilDone(); + await runUntilDone(ws); logger.trace("withdrawing test balance for refund"); const withdrawAmountTwo = Amounts.parseOrThrow(`${currency}:18`); @@ -359,25 +356,25 @@ export async function runIntegrationTest( const refundAmount = Amounts.parseOrThrow(`${currency}:6`); const spendAmountThree = Amounts.parseOrThrow(`${currency}:3`); - await wallet.withdrawTestBalance({ - amount: Amounts.stringify(withdrawAmountTwo), - bankBaseUrl: args.bankBaseUrl, - exchangeBaseUrl: args.exchangeBaseUrl, - }); + await withdrawTestBalance( + ws, + Amounts.stringify(withdrawAmountTwo), + args.bankBaseUrl, + args.exchangeBaseUrl, + ); // Wait until the withdraw is done - await wallet.runUntilDone(); + await runUntilDone(ws); const { orderId: refundOrderId } = await makePayment( - http, - wallet, + ws, myMerchant, Amounts.stringify(spendAmountTwo), "order that will be refunded", ); const refundUri = await refund( - http, + ws.http, myMerchant, refundOrderId, "test refund", @@ -386,18 +383,17 @@ export async function runIntegrationTest( logger.trace("refund URI", refundUri); - await wallet.applyRefund(refundUri); + await applyRefund(ws, refundUri); logger.trace("integration test: applied refund"); // Wait until the refund is done - await wallet.runUntilDone(); + await runUntilDone(ws); logger.trace("integration test: making payment after refund"); await makePayment( - http, - wallet, + ws, myMerchant, Amounts.stringify(spendAmountThree), "payment after refund", @@ -405,30 +401,26 @@ export async function runIntegrationTest( logger.trace("integration test: make payment done"); - await wallet.runUntilDone(); + await runUntilDone(ws); logger.trace("integration test: all done!"); } -export async function testPay( - http: HttpRequestLibrary, - wallet: Wallet, - args: TestPayArgs, -) { +export async function testPay(ws: InternalWalletState, args: TestPayArgs) { logger.trace("creating order"); const merchant = { authToken: args.merchantAuthToken, baseUrl: args.merchantBaseUrl, }; const orderResp = await createOrder( - http, + ws.http, merchant, args.amount, args.summary, "taler://fulfillment-success/thank+you", ); logger.trace("created new order with order ID", orderResp.orderId); - const checkPayResp = await checkPayment(http, merchant, orderResp.orderId); + const checkPayResp = await checkPayment(ws.http, merchant, orderResp.orderId); const talerPayUri = checkPayResp.taler_pay_uri; if (!talerPayUri) { console.error("fatal: no taler pay URI received from backend"); @@ -436,9 +428,9 @@ export async function testPay( return; } logger.trace("taler pay URI:", talerPayUri); - const result = await wallet.preparePayForUri(talerPayUri); + const result = await preparePayForUri(ws, talerPayUri); if (result.status !== PreparePayResultType.PaymentPossible) { throw Error(`unexpected prepare pay status: ${result.status}`); } - await wallet.confirmPay(result.proposalId, undefined); + await confirmPay(ws, result.proposalId, undefined); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 1b2c8477f..5836a6ee3 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -424,7 +424,7 @@ export async function retryTransaction( break; } case TransactionType.Payment: { - const proposalId = rest[0] + const proposalId = rest[0]; await processPurchasePay(ws, proposalId, true); break; } diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts index 1e87bc1f3..ed48b8dd1 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts @@ -17,7 +17,7 @@ /** * Imports. */ - import test from "ava"; +import test from "ava"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { AvailableCoinInfo, selectPayCoins } from "./coinSelection.js"; diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts index 868619ada..92a9e4396 100644 --- a/packages/taler-wallet-core/src/util/http.ts +++ b/packages/taler-wallet-core/src/util/http.ts @@ -24,7 +24,10 @@ /** * Imports */ -import { OperationFailedError, makeErrorDetails } from "../operations/errors.js"; +import { + OperationFailedError, + makeErrorDetails, +} from "../operations/errors.js"; import { Logger, Duration, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 33e431f37..82bc8b44b 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -23,33 +23,46 @@ * Imports. */ import { + AcceptBankIntegratedWithdrawalRequest, + AcceptExchangeTosRequest, + AcceptManualWithdrawalRequest, + AcceptTipRequest, + AddExchangeRequest, + ApplyRefundRequest, BackupRecovery, codecForAny, codecForDeleteTransactionRequest, codecForRetryTransactionRequest, + codecForSetWalletDeviceIdRequest, + ConfirmPayRequest, DeleteTransactionRequest, durationFromSpec, - durationMax, durationMin, + ForceRefreshRequest, getDurationRemaining, + GetExchangeTosRequest, + GetWithdrawalDetailsForAmountRequest, + GetWithdrawalDetailsForUriRequest, isTimestampExpired, j2s, + PreparePayRequest, + PrepareTipRequest, + RetryTransactionRequest, + SetCoinSuspendedRequest, + SetWalletDeviceIdRequest, TalerErrorCode, Timestamp, timestampMin, + WalletBackupContentV1, WalletCurrencyInfo, } from "@gnu-taler/taler-util"; -import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi"; import { addBackupProvider, AddBackupProviderRequest, BackupInfo, codecForAddBackupProviderRequest, - exportBackupEncrypted, getBackupInfo, getBackupRecovery, - importBackupEncrypted, - importBackupPlain, loadBackupRecovery, runBackupCycle, } from "./operations/backup"; @@ -68,7 +81,6 @@ import { import { acceptExchangeTermsOfService, getExchangeDetails, - getExchangePaytoUri, updateExchangeFromUrl, } from "./operations/exchanges"; import { @@ -76,7 +88,6 @@ import { preparePayForUri, processDownloadProposal, processPurchasePay, - refuseProposal, } from "./operations/pay"; import { getPendingOperations } from "./operations/pending"; import { processRecoupGroup } from "./operations/recoup"; @@ -93,7 +104,6 @@ import { import { createReserve, createTalerWithdrawReserve, - forceQueryReserve, getFundingPaytoUris, processReserve, } from "./operations/reserves"; @@ -104,7 +114,11 @@ import { withdrawTestBalance, } from "./operations/testing"; import { acceptTip, prepareTip, processTip } from "./operations/tip"; -import { deleteTransaction, getTransactions, retryTransaction } from "./operations/transactions"; +import { + deleteTransaction, + getTransactions, + retryTransaction, +} from "./operations/transactions"; import { getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, @@ -112,15 +126,10 @@ import { } from "./operations/withdraw"; import { AuditorTrustRecord, - CoinRecord, CoinSourceType, - ExchangeDetailsRecord, - ExchangeRecord, - ReserveRecord, ReserveRecordStatus, - WalletStoresV1, } from "./db.js"; -import { NotificationType, WalletNotification } from "@gnu-taler/taler-util"; +import { NotificationType } from "@gnu-taler/taler-util"; import { PendingOperationInfo, PendingOperationsResponse, @@ -137,7 +146,6 @@ import { AcceptWithdrawalResponse, ApplyRefundResponse, BalancesResponse, - BenchmarkResult, codecForAbortPayWithRefundRequest, codecForAcceptBankIntegratedWithdrawalRequest, codecForAcceptExchangeTosRequest, @@ -147,7 +155,6 @@ import { codecForApplyRefundRequest, codecForConfirmPayRequest, codecForCreateDepositGroupRequest, - codecForForceExchangeUpdateRequest, codecForForceRefreshRequest, codecForGetExchangeTosRequest, codecForGetWithdrawalDetailsForAmountRequest, @@ -166,28 +173,18 @@ import { ExchangeListItem, ExchangesListRespose, GetExchangeTosResult, - IntegrationTestArgs, ManualWithdrawalDetails, PreparePayResult, PrepareTipResult, RecoveryLoadRequest, RefreshReason, - ReturnCoinsRequest, - TestPayArgs, TrackDepositGroupRequest, TrackDepositGroupResponse, - WithdrawTestBalanceRequest, WithdrawUriInfoResponse, } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { assertUnreachable } from "./util/assertUnreachable"; -import { AsyncOpMemoSingle } from "./util/asyncMemo"; -import { HttpRequestLibrary } from "./util/http"; import { Logger } from "@gnu-taler/taler-util"; -import { AsyncCondition } from "./util/promiseUtils"; -import { TimerGroup } from "./util/timer"; -import { getExchangeTrust } from "./operations/currencies.js"; -import { DbAccess } from "./util/query.js"; import { setWalletDeviceId } from "./operations/backup/state.js"; const builtinAuditors: AuditorTrustRecord[] = [ @@ -201,440 +198,815 @@ const builtinAuditors: AuditorTrustRecord[] = [ const logger = new Logger("wallet.ts"); -/** - * The platform-independent wallet implementation. - */ -export class Wallet { - private ws: InternalWalletState; - private timerGroup: TimerGroup = new TimerGroup(); - private latch = new AsyncCondition(); - private stopped = false; - private memoRunRetryLoop = new AsyncOpMemoSingle(); - - get db(): DbAccess { - return this.ws.db; - } - - constructor( - db: DbAccess, - http: HttpRequestLibrary, - cryptoWorkerFactory: CryptoWorkerFactory, - ) { - this.ws = new InternalWalletState(db, http, cryptoWorkerFactory); +async function getWithdrawalDetailsForAmount( + ws: InternalWalletState, + exchangeBaseUrl: string, + amount: AmountJson, +): Promise { + const wi = await getExchangeWithdrawalInfo(ws, exchangeBaseUrl, amount); + const paytoUris = wi.exchangeDetails.wireInfo.accounts.map( + (x) => x.payto_uri, + ); + if (!paytoUris) { + throw Error("exchange is in invalid state"); } + return { + amountRaw: Amounts.stringify(amount), + amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue), + paytoUris, + tosAccepted: wi.termsOfServiceAccepted, + }; +} - getExchangePaytoUri( - exchangeBaseUrl: string, - supportedTargetTypes: string[], - ): Promise { - return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes); +/** + * Execute one operation based on the pending operation info record. + */ +async function processOnePendingOperation( + ws: InternalWalletState, + pending: PendingOperationInfo, + forceNow = false, +): Promise { + logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`); + switch (pending.type) { + case PendingOperationType.ExchangeUpdate: + await updateExchangeFromUrl(ws, pending.exchangeBaseUrl, forceNow); + break; + case PendingOperationType.Refresh: + await processRefreshGroup(ws, pending.refreshGroupId, forceNow); + break; + case PendingOperationType.Reserve: + await processReserve(ws, pending.reservePub, forceNow); + break; + case PendingOperationType.Withdraw: + await processWithdrawGroup(ws, pending.withdrawalGroupId, forceNow); + break; + case PendingOperationType.ProposalDownload: + await processDownloadProposal(ws, pending.proposalId, forceNow); + break; + case PendingOperationType.TipPickup: + await processTip(ws, pending.tipId, forceNow); + break; + case PendingOperationType.Pay: + await processPurchasePay(ws, pending.proposalId, forceNow); + break; + case PendingOperationType.RefundQuery: + await processPurchaseQueryRefund(ws, pending.proposalId, forceNow); + break; + case PendingOperationType.Recoup: + await processRecoupGroup(ws, pending.recoupGroupId, forceNow); + break; + case PendingOperationType.ExchangeCheckRefresh: + await autoRefresh(ws, pending.exchangeBaseUrl); + break; + case PendingOperationType.Deposit: + await processDepositGroup(ws, pending.depositGroupId); + break; + default: + assertUnreachable(pending); } +} - async getWithdrawalDetailsForAmount( - exchangeBaseUrl: string, - amount: AmountJson, - ): Promise { - const wi = await getExchangeWithdrawalInfo( - this.ws, - exchangeBaseUrl, - amount, - ); - const paytoUris = wi.exchangeDetails.wireInfo.accounts.map( - (x) => x.payto_uri, - ); - if (!paytoUris) { - throw Error("exchange is in invalid state"); +/** + * Process pending operations. + */ +export async function runPending( + ws: InternalWalletState, + forceNow = false, +): Promise { + const pendingOpsResponse = await getPendingOperations(ws); + for (const p of pendingOpsResponse.pendingOperations) { + if (!forceNow && !isTimestampExpired(p.timestampDue)) { + continue; } - return { - amountRaw: Amounts.stringify(amount), - amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue), - paytoUris, - tosAccepted: wi.termsOfServiceAccepted, - }; - } - - addNotificationListener(f: (n: WalletNotification) => void): void { - this.ws.addNotificationListener(f); - } - - /** - * Execute one operation based on the pending operation info record. - */ - async processOnePendingOperation( - pending: PendingOperationInfo, - forceNow = false, - ): Promise { - logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`); - switch (pending.type) { - case PendingOperationType.ExchangeUpdate: - await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow); - break; - case PendingOperationType.Refresh: - await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow); - break; - case PendingOperationType.Reserve: - await processReserve(this.ws, pending.reservePub, forceNow); - break; - case PendingOperationType.Withdraw: - await processWithdrawGroup( - this.ws, - pending.withdrawalGroupId, - forceNow, + try { + await processOnePendingOperation(ws, p, forceNow); + } catch (e) { + if (e instanceof OperationFailedAndReportedError) { + console.error( + "Operation failed:", + JSON.stringify(e.operationError, undefined, 2), ); - break; - case PendingOperationType.ProposalDownload: - await processDownloadProposal(this.ws, pending.proposalId, forceNow); - break; - case PendingOperationType.TipPickup: - await processTip(this.ws, pending.tipId, forceNow); - break; - case PendingOperationType.Pay: - await processPurchasePay(this.ws, pending.proposalId, forceNow); - break; - case PendingOperationType.RefundQuery: - await processPurchaseQueryRefund(this.ws, pending.proposalId, forceNow); - break; - case PendingOperationType.Recoup: - await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow); - break; - case PendingOperationType.ExchangeCheckRefresh: - await autoRefresh(this.ws, pending.exchangeBaseUrl); - break; - case PendingOperationType.Deposit: - await processDepositGroup(this.ws, pending.depositGroupId); - break; - default: - assertUnreachable(pending); + } else { + console.error(e); + } } } +} - /** - * Process pending operations. - */ - public async runPending(forceNow = false): Promise { - const pendingOpsResponse = await this.getPendingOperations(); - for (const p of pendingOpsResponse.pendingOperations) { - if (!forceNow && !isTimestampExpired(p.timestampDue)) { - continue; +/** + * 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. + */ +export async function runUntilDone( + ws: InternalWalletState, + req: { + maxRetries?: number; + } = {}, +): Promise { + let done = false; + const p = new Promise((resolve, reject) => { + // Monitor for conditions that means we're done or we + // should quit with an error (due to exceeded retries). + ws.addNotificationListener((n) => { + if (done) { + return; } - try { - await this.processOnePendingOperation(p, forceNow); - } catch (e) { - if (e instanceof OperationFailedAndReportedError) { - console.error( - "Operation failed:", - JSON.stringify(e.operationError, undefined, 2), - ); - } else { - console.error(e); - } + if ( + n.type === NotificationType.WaitingForRetry && + n.numGivingLiveness == 0 + ) { + done = true; + logger.trace("no liveness-giving operations left"); + resolve(); } - } - } - - /** - * 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 runUntilDone( - req: { - maxRetries?: number; - } = {}, - ): Promise { - let done = false; - const p = new Promise((resolve, reject) => { - // 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; - } - if ( - n.type === NotificationType.WaitingForRetry && - n.numGivingLiveness == 0 - ) { - done = true; - logger.trace("no liveness-giving operations left"); - resolve(); - } - const maxRetries = req.maxRetries; - if (!maxRetries) { - return; - } - this.getPendingOperations() - .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(); - } + const maxRetries = req.maxRetries; + if (!maxRetries) { + return; + } + getPendingOperations(ws) + .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}`, + ); + ws.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); - }); + } + }) + .catch((e) => { + logger.error(e); + reject(e); + }); }); - await p; - } - - /** - * Process pending operations and wait for scheduled operations in - * a loop until the wallet is stopped explicitly. - */ - public async runRetryLoop(): Promise { - // Make sure we only run one main loop at a time. - return this.memoRunRetryLoop.memo(async () => { - try { - await this.runRetryLoopImpl(); - } catch (e) { - console.error("error during retry loop execution", e); - throw e; - } + // Run this asynchronously + runRetryLoop(ws).catch((e) => { + logger.error("exception in wallet retry loop"); + reject(e); }); - } + }); + await p; +} - private async runRetryLoopImpl(): Promise { - for (let iteration = 0; !this.stopped; iteration++) { - const pending = await this.getPendingOperations(); - logger.trace(`pending operations: ${j2s(pending)}`); - let numGivingLiveness = 0; - let numDue = 0; - let minDue: Timestamp = { t_ms: "never" }; +/** + * Process pending operations and wait for scheduled operations in + * a loop until the wallet is stopped explicitly. + */ +export async function runRetryLoop(ws: InternalWalletState): Promise { + // Make sure we only run one main loop at a time. + return ws.memoRunRetryLoop.memo(async () => { + try { + await runRetryLoopImpl(ws); + } catch (e) { + console.error("error during retry loop execution", e); + throw e; + } + }); +} + +async function runRetryLoopImpl(ws: InternalWalletState): Promise { + for (let iteration = 0; !ws.stopped; iteration++) { + const pending = await getPendingOperations(ws); + logger.trace(`pending operations: ${j2s(pending)}`); + let numGivingLiveness = 0; + let numDue = 0; + let minDue: Timestamp = { t_ms: "never" }; + for (const p of pending.pendingOperations) { + minDue = timestampMin(minDue, p.timestampDue); + if (isTimestampExpired(p.timestampDue)) { + numDue++; + } + if (p.givesLifeness) { + numGivingLiveness++; + } + } + // Make sure that we run tasks that don't give lifeness at least + // one time. + if (iteration !== 0 && numDue === 0) { + // We've executed pending, due operations at least one. + // Now we don't have any more operations available, + // and need to wait. + + // Wait for at most 5 seconds to the next check. + const dt = durationMin( + durationFromSpec({ + seconds: 5, + }), + getDurationRemaining(minDue), + ); + logger.trace(`waiting for at most ${dt.d_ms} ms`); + const timeout = ws.timerGroup.resolveAfter(dt); + ws.notify({ + type: NotificationType.WaitingForRetry, + numGivingLiveness, + numPending: pending.pendingOperations.length, + }); + // Wait until either the timeout, or we are notified (via the latch) + // that more work might be available. + await Promise.race([timeout, ws.latch.wait()]); + } else { + logger.trace( + `running ${pending.pendingOperations.length} pending operations`, + ); for (const p of pending.pendingOperations) { - minDue = timestampMin(minDue, p.timestampDue); - if (isTimestampExpired(p.timestampDue)) { - numDue++; - } - if (p.givesLifeness) { - numGivingLiveness++; + if (!isTimestampExpired(p.timestampDue)) { + continue; } - } - // Make sure that we run tasks that don't give lifeness at least - // one time. - if (iteration !== 0 && numDue === 0) { - // We've executed pending, due operations at least one. - // Now we don't have any more operations available, - // and need to wait. - - // Wait for at most 5 seconds to the next check. - const dt = durationMin( - durationFromSpec({ - seconds: 5, - }), - getDurationRemaining(minDue), - ); - logger.trace(`waiting for at most ${dt.d_ms} ms`) - const timeout = this.timerGroup.resolveAfter(dt); - this.ws.notify({ - type: NotificationType.WaitingForRetry, - numGivingLiveness, - numPending: pending.pendingOperations.length, - }); - // Wait until either the timeout, or we are notified (via the latch) - // that more work might be available. - await Promise.race([timeout, this.latch.wait()]); - } else { - logger.trace( - `running ${pending.pendingOperations.length} pending operations`, - ); - for (const p of pending.pendingOperations) { - if (!isTimestampExpired(p.timestampDue)) { - continue; - } - try { - await this.processOnePendingOperation(p); - } catch (e) { - if (e instanceof OperationFailedAndReportedError) { - logger.warn("operation processed resulted in reported error"); - } else { - logger.error("Uncaught exception", e); - this.ws.notify({ - type: NotificationType.InternalError, - message: "uncaught exception", - exception: e, - }); - } + try { + await processOnePendingOperation(ws, p); + } catch (e) { + if (e instanceof OperationFailedAndReportedError) { + logger.warn("operation processed resulted in reported error"); + } else { + logger.error("Uncaught exception", e); + ws.notify({ + type: NotificationType.InternalError, + message: "uncaught exception", + exception: e, + }); } - this.ws.notify({ - type: NotificationType.PendingOperationProcessed, - }); } + ws.notify({ + type: NotificationType.PendingOperationProcessed, + }); } } - logger.trace("exiting wallet retry loop"); } + logger.trace("exiting wallet retry loop"); +} - /** - * Insert the hard-coded defaults for exchanges, coins and - * auditors into the database, unless these defaults have - * already been applied. - */ - async fillDefaults(): Promise { - await this.db - .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust })) - .runReadWrite(async (tx) => { - let applied = false; - await tx.config.iter().forEach((x) => { - if (x.key == "currencyDefaultsApplied" && x.value == true) { - applied = true; - } - }); - if (!applied) { - for (const c of builtinAuditors) { - await tx.auditorTrustStore.put(c); - } +/** + * Insert the hard-coded defaults for exchanges, coins and + * auditors into the database, unless these defaults have + * already been applied. + */ +async function fillDefaults(ws: InternalWalletState): Promise { + await ws.db + .mktx((x) => ({ config: x.config, auditorTrustStore: x.auditorTrust })) + .runReadWrite(async (tx) => { + let applied = false; + await tx.config.iter().forEach((x) => { + if (x.key == "currencyDefaultsApplied" && x.value == true) { + applied = true; } }); - } + if (!applied) { + for (const c of builtinAuditors) { + await tx.auditorTrustStore.put(c); + } + } + }); +} - /** - * Check if a payment for the given taler://pay/ URI is possible. - * - * If the payment is possible, the signature are already generated but not - * yet send to the merchant. - */ - async preparePayForUri(talerPayUri: string): Promise { - return preparePayForUri(this.ws, talerPayUri); +/** + * Create a reserve, but do not flag it as confirmed yet. + * + * Adds the corresponding exchange as a trusted exchange if it is neither + * audited nor trusted already. + */ +async function acceptManualWithdrawal( + ws: InternalWalletState, + exchangeBaseUrl: string, + amount: AmountJson, +): Promise { + try { + const resp = await createReserve(ws, { + amount, + exchange: exchangeBaseUrl, + }); + const exchangePaytoUris = await ws.db + .mktx((x) => ({ + exchanges: x.exchanges, + exchangeDetails: x.exchangeDetails, + reserves: x.reserves, + })) + .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub)); + return { + reservePub: resp.reservePub, + exchangePaytoUris, + }; + } finally { + ws.latch.trigger(); } +} - /** - * Add a contract to the wallet and sign coins, and send them. - */ - async confirmPay( - proposalId: string, - sessionIdOverride: string | undefined, - ): Promise { - try { - return await confirmPay(this.ws, proposalId, sessionIdOverride); - } finally { - this.latch.trigger(); - } +async function getExchangeTos( + ws: InternalWalletState, + exchangeBaseUrl: string, +): Promise { + const { exchange, exchangeDetails } = await updateExchangeFromUrl( + ws, + exchangeBaseUrl, + ); + const tos = exchangeDetails.termsOfServiceText; + const currentEtag = exchangeDetails.termsOfServiceLastEtag; + if (!tos || !currentEtag) { + throw Error("exchange is in invalid state"); } + return { + acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag, + currentEtag, + tos, + }; +} - /** - * First fetch information required to withdraw from the reserve, - * then deplete the reserve, withdrawing coins until it is empty. - * - * The returned promise resolves once the reserve is set to the - * state DORMANT. - */ - async processReserve(reservePub: string): Promise { - try { - return await processReserve(this.ws, reservePub); - } finally { - this.latch.trigger(); - } +async function getExchanges( + ws: InternalWalletState, +): Promise { + const exchanges: ExchangeListItem[] = []; + await ws.db + .mktx((x) => ({ + exchanges: x.exchanges, + exchangeDetails: x.exchangeDetails, + })) + .runReadOnly(async (tx) => { + const exchangeRecords = await tx.exchanges.iter().toArray(); + for (const r of exchangeRecords) { + const dp = r.detailsPointer; + if (!dp) { + continue; + } + const { currency, masterPublicKey } = dp; + const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); + if (!exchangeDetails) { + continue; + } + exchanges.push({ + exchangeBaseUrl: r.baseUrl, + currency, + paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), + }); + } + }); + return { exchanges }; +} + +async function acceptWithdrawal( + ws: InternalWalletState, + talerWithdrawUri: string, + selectedExchange: string, +): Promise { + try { + return createTalerWithdrawReserve(ws, talerWithdrawUri, selectedExchange); + } finally { + ws.latch.trigger(); } +} - /** - * Create a reserve, but do not flag it as confirmed yet. - * - * Adds the corresponding exchange as a trusted exchange if it is neither - * audited nor trusted already. - */ - async acceptManualWithdrawal( - exchangeBaseUrl: string, - amount: AmountJson, - ): Promise { - try { - const resp = await createReserve(this.ws, { - amount, - exchange: exchangeBaseUrl, - }); - const exchangePaytoUris = await this.db - .mktx((x) => ({ - exchanges: x.exchanges, - exchangeDetails: x.exchangeDetails, - reserves: x.reserves, - })) - .runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub)); - return { - reservePub: resp.reservePub, - exchangePaytoUris, - }; - } finally { - this.latch.trigger(); +/** + * Inform the wallet that the status of a reserve has changed (e.g. due to a + * confirmation from the bank.). + */ +export async function handleNotifyReserve( + ws: InternalWalletState, +): Promise { + const reserves = await ws.db + .mktx((x) => ({ + reserves: x.reserves, + })) + .runReadOnly(async (tx) => { + return tx.reserves.iter().toArray(); + }); + for (const r of reserves) { + if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) { + try { + processReserve(ws, r.reservePub); + } catch (e) { + console.error(e); + } } } +} - /** - * Check if and how an exchange is trusted and/or audited. - */ - async getExchangeTrust( - exchangeInfo: ExchangeRecord, - ): Promise<{ isTrusted: boolean; isAudited: boolean }> { - return getExchangeTrust(this.ws, exchangeInfo); - } +async function setCoinSuspended( + ws: InternalWalletState, + coinPub: string, + suspended: boolean, +): Promise { + await ws.db + .mktx((x) => ({ + coins: x.coins, + })) + .runReadWrite(async (tx) => { + const c = await tx.coins.get(coinPub); + if (!c) { + logger.warn(`coin ${coinPub} not found, won't suspend`); + return; + } + c.suspended = suspended; + await tx.coins.put(c); + }); +} - async getWithdrawalDetailsForUri( - talerWithdrawUri: string, - ): Promise { - return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri); - } +/** + * Dump the public information of coins we have in an easy-to-process format. + */ +async function dumpCoins(ws: InternalWalletState): Promise { + const coinsJson: CoinDumpJson = { coins: [] }; + await ws.db + .mktx((x) => ({ + coins: x.coins, + denominations: x.denominations, + withdrawalGroups: x.withdrawalGroups, + })) + .runReadOnly(async (tx) => { + const coins = await tx.coins.iter().toArray(); + for (const c of coins) { + const denom = await tx.denominations.get([ + c.exchangeBaseUrl, + c.denomPubHash, + ]); + if (!denom) { + console.error("no denom session found for coin"); + continue; + } + const cs = c.coinSource; + let refreshParentCoinPub: string | undefined; + if (cs.type == CoinSourceType.Refresh) { + refreshParentCoinPub = cs.oldCoinPub; + } + let withdrawalReservePub: string | undefined; + if (cs.type == CoinSourceType.Withdraw) { + const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId); + if (!ws) { + console.error("no withdrawal session found for coin"); + continue; + } + withdrawalReservePub = ws.reservePub; + } + coinsJson.coins.push({ + coin_pub: c.coinPub, + denom_pub: c.denomPub, + denom_pub_hash: c.denomPubHash, + denom_value: Amounts.stringify(denom.value), + exchange_base_url: c.exchangeBaseUrl, + refresh_parent_coin_pub: refreshParentCoinPub, + remaining_value: Amounts.stringify(c.currentAmount), + withdrawal_reserve_pub: withdrawalReservePub, + coin_suspended: c.suspended, + }); + } + }); + return coinsJson; +} - async deleteTransaction(req: DeleteTransactionRequest): Promise { - return deleteTransaction(this.ws, req.transactionId); - } +export enum WalletApiOperation { + InitWallet = "initWallet", + WithdrawTestkudos = "withdrawTestkudos", + WithdrawTestBalance = "withdrawTestBalance", + PreparePayForUri = "preparePayForUri", + RunIntegrationTest = "runIntegrationTest", + TestPay = "testPay", + AddExchange = "addExchange", + GetTransactions = "getTransactions", + ListExchanges = "listExchanges", + GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri", + GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount", + AcceptManualWithdrawal = "acceptManualWithdrawal", + GetBalances = "getBalances", + GetPendingOperations = "getPendingOperations", + SetExchangeTosAccepted = "setExchangeTosAccepted", + ApplyRefund = "applyRefund", + AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal", + GetExchangeTos = "getExchangeTos", + RetryPendingNow = "retryPendingNow", + PreparePay = "preparePay", + ConfirmPay = "confirmPay", + DumpCoins = "dumpCoins", + SetCoinSuspended = "setCoinSuspended", + ForceRefresh = "forceRefresh", + PrepareTip = "prepareTip", + AcceptTip = "acceptTip", + ExportBackup = "exportBackup", + AddBackupProvider = "addBackupProvider", + RunBackupCycle = "runBackupCycle", + ExportBackupRecovery = "exportBackupRecovery", + ImportBackupRecovery = "importBackupRecovery", + GetBackupInfo = "getBackupInfo", + TrackDepositGroup = "trackDepositGroup", + DeleteTransaction = "deleteTransaction", + RetryTransaction = "retryTransaction", + GetCoins = "getCoins", + ListCurrencies = "listCurrencies", + CreateDepositGroup = "createDepositGroup", + SetWalletDeviceId = "setWalletDeviceId", + ExportBackupPlain = "exportBackupPlain", +} - async setDeviceId(newDeviceId: string): Promise { - return setWalletDeviceId(this.ws, newDeviceId); - } +export type WalletOperations = { + [WalletApiOperation.PreparePayForUri]: { + request: PreparePayRequest; + response: PreparePayResult; + }; + [WalletApiOperation.WithdrawTestkudos]: { + request: {}; + response: {}; + }; + [WalletApiOperation.PreparePay]: { + request: PreparePayRequest; + response: PreparePayResult; + }; + [WalletApiOperation.ConfirmPay]: { + request: ConfirmPayRequest; + response: ConfirmPayResult; + }; + [WalletApiOperation.GetBalances]: { + request: {}; + response: BalancesResponse; + }; + [WalletApiOperation.GetTransactions]: { + request: TransactionsRequest; + response: TransactionsResponse; + }; + [WalletApiOperation.GetPendingOperations]: { + request: {}; + response: PendingOperationsResponse; + }; + [WalletApiOperation.DumpCoins]: { + request: {}; + response: CoinDumpJson; + }; + [WalletApiOperation.SetCoinSuspended]: { + request: SetCoinSuspendedRequest; + response: {}; + }; + [WalletApiOperation.ForceRefresh]: { + request: ForceRefreshRequest; + response: {}; + }; + [WalletApiOperation.DeleteTransaction]: { + request: DeleteTransactionRequest; + response: {}; + }; + [WalletApiOperation.RetryTransaction]: { + request: RetryTransactionRequest; + response: {}; + }; + [WalletApiOperation.PrepareTip]: { + request: PrepareTipRequest; + response: PrepareTipResult; + }; + [WalletApiOperation.AcceptTip]: { + request: AcceptTipRequest; + response: {}; + }; + [WalletApiOperation.ApplyRefund]: { + request: ApplyRefundRequest; + response: ApplyRefundResponse; + }; + [WalletApiOperation.ListCurrencies]: { + request: {}; + response: WalletCurrencyInfo; + }; + [WalletApiOperation.GetWithdrawalDetailsForAmount]: { + request: GetWithdrawalDetailsForAmountRequest; + response: ManualWithdrawalDetails; + }; + [WalletApiOperation.GetWithdrawalDetailsForUri]: { + request: GetWithdrawalDetailsForUriRequest; + response: WithdrawUriInfoResponse; + }; + [WalletApiOperation.AcceptBankIntegratedWithdrawal]: { + request: AcceptBankIntegratedWithdrawalRequest; + response: AcceptWithdrawalResponse; + }; + [WalletApiOperation.AcceptManualWithdrawal]: { + request: AcceptManualWithdrawalRequest; + response: AcceptManualWithdrawalResult; + }; + [WalletApiOperation.ListExchanges]: { + request: {}; + response: ExchangesListRespose; + }; + [WalletApiOperation.AddExchange]: { + request: AddExchangeRequest; + response: {}; + }; + [WalletApiOperation.SetExchangeTosAccepted]: { + request: AcceptExchangeTosRequest; + response: {}; + }; + [WalletApiOperation.GetExchangeTos]: { + request: GetExchangeTosRequest; + response: GetExchangeTosResult; + }; + [WalletApiOperation.TrackDepositGroup]: { + request: TrackDepositGroupRequest; + response: TrackDepositGroupResponse; + }; + [WalletApiOperation.CreateDepositGroup]: { + request: CreateDepositGroupRequest; + response: CreateDepositGroupResponse; + }; + [WalletApiOperation.SetWalletDeviceId]: { + request: SetWalletDeviceIdRequest; + response: {}; + }; + [WalletApiOperation.ExportBackupPlain]: { + request: {}; + response: WalletBackupContentV1; + }; + [WalletApiOperation.ExportBackupRecovery]: { + request: {}; + response: BackupRecovery; + }; + [WalletApiOperation.ImportBackupRecovery]: { + request: RecoveryLoadRequest; + response: {}; + }; + [WalletApiOperation.RunBackupCycle]: { + request: {}; + response: {}; + }; + [WalletApiOperation.AddBackupProvider]: { + request: AddBackupProviderRequest; + response: {}; + }; + [WalletApiOperation.GetBackupInfo]: { + request: {}; + response: BackupInfo; + }; +}; + +export type RequestType< + Op extends WalletApiOperation & keyof WalletOperations +> = WalletOperations[Op] extends { request: infer T } ? T : never; + +export type ResponseType< + Op extends WalletApiOperation & keyof WalletOperations +> = WalletOperations[Op] extends { response: infer T } ? T : never; + +export interface WalletCoreApiClient { + call( + operation: Op, + payload: RequestType, + ): Promise>; +} - /** - * Update or add exchange DB entry by fetching the /keys and /wire information. - */ - async updateExchangeFromUrl( - baseUrl: string, - force = false, - ): Promise<{ - exchange: ExchangeRecord; - exchangeDetails: ExchangeDetailsRecord; - }> { - try { - return updateExchangeFromUrl(this.ws, baseUrl, force); - } finally { - this.latch.trigger(); - } - } +/** + * Get an API client from an internal wallet state object. + */ +export async function getClientFromWalletState( + ws: InternalWalletState, +): Promise { + let id = 0; + const client: WalletCoreApiClient = { + async call(op, payload): Promise { + const res = await handleCoreApiRequest(ws, op, `${id++}`, payload); + switch (res.type) { + case "error": + throw new OperationFailedError(res.error); + case "response": + return res.result; + } + }, + }; + return client; +} - async getExchangeTos(exchangeBaseUrl: string): Promise { - const { exchange, exchangeDetails } = await this.updateExchangeFromUrl( - exchangeBaseUrl, +/** + * Implementation of the "wallet-core" API. + */ +async function dispatchRequestInternal( + ws: InternalWalletState, + operation: string, + payload: unknown, +): Promise> { + if (ws.initCalled && operation !== "initWallet") { + throw Error( + `wallet must be initialized before running operation ${operation}`, ); - const tos = exchangeDetails.termsOfServiceText; - const currentEtag = exchangeDetails.termsOfServiceLastEtag; - if (!tos || !currentEtag) { - throw Error("exchange is in invalid state"); - } - return { - acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag, - currentEtag, - tos, - }; } - - /** - * Get detailed balance information, sliced by exchange and by currency. - */ - async getBalances(): Promise { - return this.ws.memoGetBalance.memo(() => getBalances(this.ws)); - } - - async refresh(oldCoinPub: string): Promise { - try { - const refreshGroupId = await this.db + switch (operation) { + case "initWallet": { + ws.initCalled = true; + return {}; + } + case "withdrawTestkudos": { + await withdrawTestBalance( + ws, + "TESTKUDOS:10", + "https://bank.test.taler.net/", + "https://exchange.test.taler.net/", + ); + return {}; + } + case "withdrawTestBalance": { + const req = codecForWithdrawTestBalance().decode(payload); + await withdrawTestBalance( + ws, + req.amount, + req.bankBaseUrl, + req.exchangeBaseUrl, + ); + return {}; + } + case "runIntegrationTest": { + const req = codecForIntegrationTestArgs().decode(payload); + await runIntegrationTest(ws, req); + return {}; + } + case "testPay": { + const req = codecForTestPayArgs().decode(payload); + await testPay(ws, req); + return {}; + } + case "getTransactions": { + const req = codecForTransactionsRequest().decode(payload); + return await getTransactions(ws, req); + } + case "addExchange": { + const req = codecForAddExchangeRequest().decode(payload); + await updateExchangeFromUrl(ws, req.exchangeBaseUrl, req.forceUpdate); + return {}; + } + case "listExchanges": { + return await getExchanges(ws); + } + case "getWithdrawalDetailsForUri": { + const req = codecForGetWithdrawalDetailsForUri().decode(payload); + return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri); + } + case "acceptManualWithdrawal": { + const req = codecForAcceptManualWithdrawalRequet().decode(payload); + const res = await acceptManualWithdrawal( + ws, + req.exchangeBaseUrl, + Amounts.parseOrThrow(req.amount), + ); + return res; + } + case "getWithdrawalDetailsForAmount": { + const req = codecForGetWithdrawalDetailsForAmountRequest().decode( + payload, + ); + return await getWithdrawalDetailsForAmount( + ws, + req.exchangeBaseUrl, + Amounts.parseOrThrow(req.amount), + ); + } + case "getBalances": { + return await getBalances(ws); + } + case "getPendingOperations": { + return await getPendingOperations(ws); + } + case "setExchangeTosAccepted": { + const req = codecForAcceptExchangeTosRequest().decode(payload); + await acceptExchangeTermsOfService(ws, req.exchangeBaseUrl, req.etag); + return {}; + } + case "applyRefund": { + const req = codecForApplyRefundRequest().decode(payload); + return await applyRefund(ws, req.talerRefundUri); + } + case "acceptBankIntegratedWithdrawal": { + const req = codecForAcceptBankIntegratedWithdrawalRequest().decode( + payload, + ); + return await acceptWithdrawal( + ws, + req.talerWithdrawUri, + req.exchangeBaseUrl, + ); + } + case "getExchangeTos": { + const req = codecForGetExchangeTosRequest().decode(payload); + return getExchangeTos(ws, req.exchangeBaseUrl); + } + case "retryPendingNow": { + await runPending(ws, true); + return {}; + } + case "preparePay": { + const req = codecForPreparePayRequest().decode(payload); + return await preparePayForUri(ws, req.talerPayUri); + } + case "confirmPay": { + const req = codecForConfirmPayRequest().decode(payload); + return await confirmPay(ws, req.proposalId, req.sessionId); + } + case "abortFailedPayWithRefund": { + const req = codecForAbortPayWithRefundRequest().decode(payload); + await abortFailedPayWithRefund(ws, req.proposalId); + return {}; + } + case "dumpCoins": { + return await dumpCoins(ws); + } + case "setCoinSuspended": { + const req = codecForSetCoinSuspendedRequest().decode(payload); + await setCoinSuspended(ws, req.coinPub, req.suspended); + return {}; + } + case "forceRefresh": { + const req = codecForForceRefreshRequest().decode(payload); + const coinPubs = req.coinPubList.map((x) => ({ coinPub: x })); + const refreshGroupId = await ws.db .mktx((x) => ({ refreshGroups: x.refreshGroups, denominations: x.denominations, @@ -642,613 +1014,150 @@ export class Wallet { })) .runReadWrite(async (tx) => { return await createRefreshGroup( - this.ws, + ws, tx, - [{ coinPub: oldCoinPub }], + coinPubs, RefreshReason.Manual, ); }); - await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId); - } catch (e) { - this.latch.trigger(); + processRefreshGroup(ws, refreshGroupId.refreshGroupId, true).catch( + (x) => { + logger.error(x); + }, + ); + return { + refreshGroupId, + }; } - } - - async getPendingOperations(): Promise { - return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws)); - } - - async acceptExchangeTermsOfService( - exchangeBaseUrl: string, - etag: string | undefined, - ): Promise { - return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag); - } - - async getExchanges(): Promise { - const exchanges: ExchangeListItem[] = []; - await this.db - .mktx((x) => ({ - exchanges: x.exchanges, - exchangeDetails: x.exchangeDetails, - })) - .runReadOnly(async (tx) => { - const exchangeRecords = await tx.exchanges.iter().toArray(); - for (const r of exchangeRecords) { - const dp = r.detailsPointer; - if (!dp) { - continue; - } - const { currency, masterPublicKey } = dp; - const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); - if (!exchangeDetails) { - continue; - } - exchanges.push({ - exchangeBaseUrl: r.baseUrl, - currency, - paytoUris: exchangeDetails.wireInfo.accounts.map( - (x) => x.payto_uri, - ), - }); - } - }); - return { exchanges }; - } - - async getCurrencies(): Promise { - return await this.ws.db - .mktx((x) => ({ - auditorTrust: x.auditorTrust, - exchangeTrust: x.exchangeTrust, - })) - .runReadOnly(async (tx) => { - const trustedAuditors = await tx.auditorTrust.iter().toArray(); - const trustedExchanges = await tx.exchangeTrust.iter().toArray(); - return { - trustedAuditors: trustedAuditors.map((x) => ({ - currency: x.currency, - auditorBaseUrl: x.auditorBaseUrl, - auditorPub: x.auditorPub, - })), - trustedExchanges: trustedExchanges.map((x) => ({ - currency: x.currency, - exchangeBaseUrl: x.exchangeBaseUrl, - exchangeMasterPub: x.exchangeMasterPub, - })), - }; - }); - } - - /** - * Stop ongoing processing. - */ - stop(): void { - this.stopped = true; - this.timerGroup.stopCurrentAndFutureTimers(); - this.ws.cryptoApi.stop(); - } - - /** - * Trigger paying coins back into the user's account. - */ - async returnCoins(req: ReturnCoinsRequest): Promise { - throw Error("not implemented"); - } - - /** - * Accept a refund, return the contract hash for the contract - * that was involved in the refund. - */ - async applyRefund(talerRefundUri: string): Promise { - return applyRefund(this.ws, talerRefundUri); - } - - async acceptTip(talerTipUri: string): Promise { - try { - return acceptTip(this.ws, talerTipUri); - } catch (e) { - this.latch.trigger(); + case "prepareTip": { + const req = codecForPrepareTipRequest().decode(payload); + return await prepareTip(ws, req.talerTipUri); } - } - - async prepareTip(talerTipUri: string): Promise { - return prepareTip(this.ws, talerTipUri); - } - - async abortFailedPayWithRefund(proposalId: string): Promise { - return abortFailedPayWithRefund(this.ws, proposalId); - } - - /** - * Inform the wallet that the status of a reserve has changed (e.g. due to a - * confirmation from the bank.). - */ - public async handleNotifyReserve(): Promise { - const reserves = await this.ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.iter().toArray(); - }); - for (const r of reserves) { - if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) { - try { - this.processReserve(r.reservePub); - } catch (e) { - console.error(e); - } - } + case "acceptTip": { + const req = codecForAcceptTipRequest().decode(payload); + await acceptTip(ws, req.walletTipId); + return {}; } - } - - /** - * Remove unreferenced / expired data from the wallet's database - * based on the current system time. - */ - async collectGarbage(): Promise { - // FIXME(#5845) - // We currently do not garbage-collect the wallet database. This might change - // after the feature has been properly re-designed, and we have come up with a - // strategy to test it. - } - - async acceptWithdrawal( - talerWithdrawUri: string, - selectedExchange: string, - ): Promise { - try { - return createTalerWithdrawReserve( - this.ws, - talerWithdrawUri, - selectedExchange, - ); - } finally { - this.latch.trigger(); + case "exportBackupPlain": { + return exportBackup(ws); } - } - - async refuseProposal(proposalId: string): Promise { - return refuseProposal(this.ws, proposalId); - } - - benchmarkCrypto(repetitions: number): Promise { - return this.ws.cryptoApi.benchmark(repetitions); - } - - async setCoinSuspended(coinPub: string, suspended: boolean): Promise { - await this.db - .mktx((x) => ({ - coins: x.coins, - })) - .runReadWrite(async (tx) => { - const c = await tx.coins.get(coinPub); - if (!c) { - logger.warn(`coin ${coinPub} not found, won't suspend`); - return; - } - c.suspended = suspended; - await tx.coins.put(c); - }); - } - - /** - * Dump the public information of coins we have in an easy-to-process format. - */ - async dumpCoins(): Promise { - const coinsJson: CoinDumpJson = { coins: [] }; - await this.ws.db - .mktx((x) => ({ - coins: x.coins, - denominations: x.denominations, - withdrawalGroups: x.withdrawalGroups, - })) - .runReadOnly(async (tx) => { - const coins = await tx.coins.iter().toArray(); - for (const c of coins) { - const denom = await tx.denominations.get([ - c.exchangeBaseUrl, - c.denomPubHash, - ]); - if (!denom) { - console.error("no denom session found for coin"); - continue; - } - const cs = c.coinSource; - let refreshParentCoinPub: string | undefined; - if (cs.type == CoinSourceType.Refresh) { - refreshParentCoinPub = cs.oldCoinPub; - } - let withdrawalReservePub: string | undefined; - if (cs.type == CoinSourceType.Withdraw) { - const ws = await tx.withdrawalGroups.get(cs.withdrawalGroupId); - if (!ws) { - console.error("no withdrawal session found for coin"); - continue; - } - withdrawalReservePub = ws.reservePub; - } - coinsJson.coins.push({ - coin_pub: c.coinPub, - denom_pub: c.denomPub, - denom_pub_hash: c.denomPubHash, - denom_value: Amounts.stringify(denom.value), - exchange_base_url: c.exchangeBaseUrl, - refresh_parent_coin_pub: refreshParentCoinPub, - remaining_value: Amounts.stringify(c.currentAmount), - withdrawal_reserve_pub: withdrawalReservePub, - coin_suspended: c.suspended, - }); - } - }); - return coinsJson; - } - - async getTransactions( - request: TransactionsRequest, - ): Promise { - return getTransactions(this.ws, request); - } - - async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise { - await withdrawTestBalance( - this.ws, - req.amount, - req.bankBaseUrl, - req.exchangeBaseUrl, - ); - } - - async updateReserve(reservePub: string): Promise { - await forceQueryReserve(this.ws, reservePub); - return await this.ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(reservePub); - }); - } - - async getCoins(): Promise { - return await this.db - .mktx((x) => ({ - coins: x.coins, - })) - .runReadOnly(async (tx) => { - return tx.coins.iter().toArray(); - }); - } - - async getReservesForExchange( - exchangeBaseUrl?: string, - ): Promise { - return await this.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - if (exchangeBaseUrl) { - return await tx.reserves - .iter() - .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl); - } else { - return await tx.reserves.iter().toArray(); - } - }); - } - - async getReserve(reservePub: string): Promise { - return await this.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(reservePub); - }); - } - - async runIntegrationtest(args: IntegrationTestArgs): Promise { - return runIntegrationTest(this.ws.http, this, args); - } - - async testPay(args: TestPayArgs) { - return testPay(this.ws.http, this, args); - } - - async exportBackupPlain() { - return exportBackup(this.ws); - } - - async importBackupPlain(backup: any) { - return importBackupPlain(this.ws, backup); - } - - async exportBackupEncrypted() { - return exportBackupEncrypted(this.ws); - } - - async importBackupEncrypted(backup: Uint8Array) { - return importBackupEncrypted(this.ws, backup); - } - - async getBackupRecovery(): Promise { - return getBackupRecovery(this.ws); - } - - async loadBackupRecovery(req: RecoveryLoadRequest): Promise { - return loadBackupRecovery(this.ws, req); - } - - async addBackupProvider(req: AddBackupProviderRequest): Promise { - return addBackupProvider(this.ws, req); - } - - async createDepositGroup( - req: CreateDepositGroupRequest, - ): Promise { - return createDepositGroup(this.ws, req); - } - - async runBackupCycle(): Promise { - return runBackupCycle(this.ws); - } - - async getBackupStatus(): Promise { - return getBackupInfo(this.ws); - } - - async trackDepositGroup( - req: TrackDepositGroupRequest, - ): Promise { - return trackDepositGroup(this.ws, req); - } - - /** - * Implementation of the "wallet-core" API. - */ - private async dispatchRequestInternal( - operation: string, - payload: unknown, - ): Promise> { - switch (operation) { - case "withdrawTestkudos": { - await this.withdrawTestBalance({ - amount: "TESTKUDOS:10", - bankBaseUrl: "https://bank.test.taler.net/", - exchangeBaseUrl: "https://exchange.test.taler.net/", + case "addBackupProvider": { + const req = codecForAddBackupProviderRequest().decode(payload); + await addBackupProvider(ws, req); + return {}; + } + case "runBackupCycle": { + await runBackupCycle(ws); + return {}; + } + case "exportBackupRecovery": { + const resp = await getBackupRecovery(ws); + return resp; + } + case "importBackupRecovery": { + const req = codecForAny().decode(payload); + await loadBackupRecovery(ws, req); + return {}; + } + case "getBackupInfo": { + const resp = await getBackupInfo(ws); + return resp; + } + case "createDepositGroup": { + const req = codecForCreateDepositGroupRequest().decode(payload); + return await createDepositGroup(ws, req); + } + case "trackDepositGroup": { + const req = codecForTrackDepositGroupRequest().decode(payload); + return trackDepositGroup(ws, req); + } + case "deleteTransaction": { + const req = codecForDeleteTransactionRequest().decode(payload); + await deleteTransaction(ws, req.transactionId); + return {}; + } + case "retryTransaction": { + const req = codecForRetryTransactionRequest().decode(payload); + await retryTransaction(ws, req.transactionId); + return {}; + } + case "setWalletDeviceId": { + const req = codecForSetWalletDeviceIdRequest().decode(payload); + await setWalletDeviceId(ws, req.walletDeviceId); + return {}; + } + case "listCurrencies": { + return await ws.db + .mktx((x) => ({ + auditorTrust: x.auditorTrust, + exchangeTrust: x.exchangeTrust, + })) + .runReadOnly(async (tx) => { + const trustedAuditors = await tx.auditorTrust.iter().toArray(); + const trustedExchanges = await tx.exchangeTrust.iter().toArray(); + return { + trustedAuditors: trustedAuditors.map((x) => ({ + currency: x.currency, + auditorBaseUrl: x.auditorBaseUrl, + auditorPub: x.auditorPub, + })), + trustedExchanges: trustedExchanges.map((x) => ({ + currency: x.currency, + exchangeBaseUrl: x.exchangeBaseUrl, + exchangeMasterPub: x.exchangeMasterPub, + })), + }; }); - return {}; - } - case "withdrawTestBalance": { - const req = codecForWithdrawTestBalance().decode(payload); - await this.withdrawTestBalance(req); - return {}; - } - case "runIntegrationTest": { - const req = codecForIntegrationTestArgs().decode(payload); - await this.runIntegrationtest(req); - return {}; - } - case "testPay": { - const req = codecForTestPayArgs().decode(payload); - await this.testPay(req); - return {}; - } - case "getTransactions": { - const req = codecForTransactionsRequest().decode(payload); - return await this.getTransactions(req); - } - case "addExchange": { - const req = codecForAddExchangeRequest().decode(payload); - await this.updateExchangeFromUrl(req.exchangeBaseUrl); - return {}; - } - case "forceUpdateExchange": { - const req = codecForForceExchangeUpdateRequest().decode(payload); - await this.updateExchangeFromUrl(req.exchangeBaseUrl, true); - return {}; - } - case "listExchanges": { - return await this.getExchanges(); - } - case "getWithdrawalDetailsForUri": { - const req = codecForGetWithdrawalDetailsForUri().decode(payload); - return await this.getWithdrawalDetailsForUri(req.talerWithdrawUri); - } - case "acceptManualWithdrawal": { - const req = codecForAcceptManualWithdrawalRequet().decode(payload); - const res = await this.acceptManualWithdrawal( - req.exchangeBaseUrl, - Amounts.parseOrThrow(req.amount), - ); - return res; - } - case "getWithdrawalDetailsForAmount": { - const req = codecForGetWithdrawalDetailsForAmountRequest().decode( - payload, - ); - return await this.getWithdrawalDetailsForAmount( - req.exchangeBaseUrl, - Amounts.parseOrThrow(req.amount), - ); - } - case "getBalances": { - return await this.getBalances(); - } - case "getPendingOperations": { - return await this.getPendingOperations(); - } - case "setExchangeTosAccepted": { - const req = codecForAcceptExchangeTosRequest().decode(payload); - await this.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag); - return {}; - } - case "applyRefund": { - const req = codecForApplyRefundRequest().decode(payload); - return await this.applyRefund(req.talerRefundUri); - } - case "acceptBankIntegratedWithdrawal": { - const req = codecForAcceptBankIntegratedWithdrawalRequest().decode( - payload, - ); - return await this.acceptWithdrawal( - req.talerWithdrawUri, - req.exchangeBaseUrl, - ); - } - case "getExchangeTos": { - const req = codecForGetExchangeTosRequest().decode(payload); - return this.getExchangeTos(req.exchangeBaseUrl); - } - case "retryPendingNow": { - await this.runPending(true); - return {}; - } - case "preparePay": { - const req = codecForPreparePayRequest().decode(payload); - return await this.preparePayForUri(req.talerPayUri); - } - case "confirmPay": { - const req = codecForConfirmPayRequest().decode(payload); - return await this.confirmPay(req.proposalId, req.sessionId); - } - case "abortFailedPayWithRefund": { - const req = codecForAbortPayWithRefundRequest().decode(payload); - await this.abortFailedPayWithRefund(req.proposalId); - return {}; - } - case "dumpCoins": { - return await this.dumpCoins(); - } - case "setCoinSuspended": { - const req = codecForSetCoinSuspendedRequest().decode(payload); - await this.setCoinSuspended(req.coinPub, req.suspended); - return {}; - } - case "forceRefresh": { - const req = codecForForceRefreshRequest().decode(payload); - const coinPubs = req.coinPubList.map((x) => ({ coinPub: x })); - const refreshGroupId = await this.db - .mktx((x) => ({ - refreshGroups: x.refreshGroups, - denominations: x.denominations, - coins: x.coins, - })) - .runReadWrite(async (tx) => { - return await createRefreshGroup( - this.ws, - tx, - coinPubs, - RefreshReason.Manual, - ); - }); - return { - refreshGroupId, - }; - } - case "prepareTip": { - const req = codecForPrepareTipRequest().decode(payload); - return await this.prepareTip(req.talerTipUri); - } - case "acceptTip": { - const req = codecForAcceptTipRequest().decode(payload); - await this.acceptTip(req.walletTipId); - return {}; - } - case "exportBackup": { - return exportBackup(this.ws); - } - case "addBackupProvider": { - const req = codecForAddBackupProviderRequest().decode(payload); - await addBackupProvider(this.ws, req); - return {}; - } - case "runBackupCycle": { - await runBackupCycle(this.ws); - return {}; - } - case "exportBackupRecovery": { - const resp = await getBackupRecovery(this.ws); - return resp; - } - case "importBackupRecovery": { - const req = codecForAny().decode(payload); - await loadBackupRecovery(this.ws, req); - return {}; - } - case "getBackupInfo": { - const resp = await getBackupInfo(this.ws); - return resp; - } - case "createDepositGroup": { - const req = codecForCreateDepositGroupRequest().decode(payload); - return await createDepositGroup(this.ws, req); - } - case "trackDepositGroup": { - const req = codecForTrackDepositGroupRequest().decode(payload); - return trackDepositGroup(this.ws, req); - } - case "deleteTransaction": { - const req = codecForDeleteTransactionRequest().decode(payload); - await deleteTransaction(this.ws, req.transactionId); - return {}; - } - case "retryTransaction": { - const req = codecForRetryTransactionRequest().decode(payload); - await retryTransaction(this.ws, req.transactionId); - return {}; - } } - throw OperationFailedError.fromCode( - TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, - "unknown operation", - { - operation, - }, - ); } + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, + "unknown operation", + { + operation, + }, + ); +} - /** - * Handle a request to the wallet-core API. - */ - async handleCoreApiRequest( - operation: string, - id: string, - payload: unknown, - ): Promise { - try { - const result = await this.dispatchRequestInternal(operation, payload); +/** + * Handle a request to the wallet-core API. + */ +export async function handleCoreApiRequest( + ws: InternalWalletState, + operation: string, + id: string, + payload: unknown, +): Promise { + try { + const result = await dispatchRequestInternal(ws, operation, payload); + return { + type: "response", + operation, + id, + result, + }; + } catch (e) { + if ( + e instanceof OperationFailedError || + e instanceof OperationFailedAndReportedError + ) { return { - type: "response", + type: "error", operation, id, - result, + error: e.operationError, + }; + } else { + return { + type: "error", + operation, + id, + error: makeErrorDetails( + TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, + `unexpected exception: ${e}`, + {}, + ), }; - } catch (e) { - if ( - e instanceof OperationFailedError || - e instanceof OperationFailedAndReportedError - ) { - return { - type: "error", - operation, - id, - error: e.operationError, - }; - } else { - return { - type: "error", - operation, - id, - error: makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION, - `unexpected exception: ${e}`, - {}, - ), - }; - } } } } diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index 51a44ee67..d3f99d9cb 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -26,7 +26,6 @@ import { isFirefox, getPermissionsApi } from "./compat"; import { extendedPermissions } from "./permissions"; import { - Wallet, OpenedPromise, openPromise, openTalerDatabase, @@ -34,6 +33,9 @@ import { deleteTalerDatabase, DbAccess, WalletStoresV1, + handleCoreApiRequest, + runRetryLoop, + handleNotifyReserve, } from "@gnu-taler/taler-wallet-core"; import { classifyTalerUri, @@ -45,12 +47,13 @@ import { } from "@gnu-taler/taler-util"; import { BrowserHttpLib } from "./browserHttpLib"; import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory"; +import { InternalWalletState } from "@gnu-taler/taler-wallet-core/src/operations/state"; /** * Currently active wallet instance. Might be unloaded and * re-instantiated when the database is reset. */ -let currentWallet: Wallet | undefined; +let currentWallet: InternalWalletState | undefined; let currentDatabase: DbAccess | undefined; @@ -167,7 +170,7 @@ async function dispatch( }; break; } - r = await w.handleCoreApiRequest(req.operation, req.id, req.payload); + r = await handleCoreApiRequest(w, req.operation, req.id, req.payload); break; } } @@ -253,7 +256,7 @@ async function reinitWallet(): Promise { } const http = new BrowserHttpLib(); console.log("setting wallet"); - const wallet = new Wallet( + const wallet = new InternalWalletState( currentDatabase, http, new BrowserCryptoWorkerFactory(), @@ -267,7 +270,7 @@ async function reinitWallet(): Promise { } } }); - wallet.runRetryLoop().catch((e) => { + runRetryLoop(wallet).catch((e) => { console.log("error during wallet retry loop", e); }); // Useful for debugging in the background page. @@ -357,7 +360,7 @@ function headerListener( if (!w) { return; } - w.handleNotifyReserve(); + handleNotifyReserve(w); }); break; default: @@ -448,3 +451,4 @@ export async function wxMain(): Promise { setupHeaderListener(); }); } + -- cgit v1.2.3