/* This file is part of GNU Taler (C) 2015-2019 GNUnet e.V. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ /** * High-level wallet operations that should be indepentent from the underlying * browser extension interface. */ /** * Imports. */ import { TalerErrorCode } from "."; import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi"; import { addBackupProvider, AddBackupProviderRequest, BackupInfo, BackupRecovery, codecForAddBackupProviderRequest, exportBackup, exportBackupEncrypted, getBackupInfo, getBackupRecovery, importBackupEncrypted, importBackupPlain, loadBackupRecovery, runBackupCycle, } from "./operations/backup"; import { getBalances } from "./operations/balance"; import { createDepositGroup, processDepositGroup, trackDepositGroup, } from "./operations/deposits"; import { makeErrorDetails, OperationFailedAndReportedError, OperationFailedError, } from "./operations/errors"; import { acceptExchangeTermsOfService, getExchangePaytoUri, getExchangeTrust, updateExchangeFromUrl, } from "./operations/exchanges"; import { confirmPay, preparePayForUri, processDownloadProposal, processPurchasePay, refuseProposal, } from "./operations/pay"; import { getPendingOperations } from "./operations/pending"; import { processRecoupGroup } from "./operations/recoup"; import { autoRefresh, createRefreshGroup, processRefreshGroup, } from "./operations/refresh"; import { abortFailedPayWithRefund, applyRefund, processPurchaseQueryRefund, } from "./operations/refund"; import { createReserve, createTalerWithdrawReserve, forceQueryReserve, getFundingPaytoUris, processReserve, } from "./operations/reserves"; import { InternalWalletState } from "./operations/state"; import { runIntegrationTest, testPay, withdrawTestBalance, } from "./operations/testing"; import { acceptTip, prepareTip, processTip } from "./operations/tip"; import { getTransactions } from "./operations/transactions"; import { getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, processWithdrawGroup, } from "./operations/withdraw"; import { CoinRecord, CoinSourceType, CurrencyRecord, DenominationRecord, ExchangeRecord, PurchaseRecord, RefundState, ReserveRecord, ReserveRecordStatus, Stores, } from "./types/dbTypes"; import { NotificationType, WalletNotification } from "./types/notifications"; import { PendingOperationInfo, PendingOperationsResponse, PendingOperationType, } from "./types/pendingTypes"; import { CoinDumpJson } from "./types/talerTypes"; import { codecForTransactionsRequest, TransactionsRequest, TransactionsResponse, } from "./types/transactionsTypes"; import { AcceptManualWithdrawalResult, AcceptWithdrawalResponse, ApplyRefundResponse, BalancesResponse, BenchmarkResult, codecForAbortPayWithRefundRequest, codecForAcceptBankIntegratedWithdrawalRequest, codecForAcceptExchangeTosRequest, codecForAcceptManualWithdrawalRequet, codecForAcceptTipRequest, codecForAddExchangeRequest, codecForApplyRefundRequest, codecForConfirmPayRequest, codecForCreateDepositGroupRequest, codecForForceExchangeUpdateRequest, codecForForceRefreshRequest, codecForGetExchangeTosRequest, codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, codecForIntegrationTestArgs, codecForPreparePayRequest, codecForPrepareTipRequest, codecForSetCoinSuspendedRequest, codecForTestPayArgs, codecForTrackDepositGroupRequest, codecForWithdrawTestBalance, ConfirmPayResult, CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, ExchangeListItem, ExchangesListRespose, GetExchangeTosResult, IntegrationTestArgs, ManualWithdrawalDetails, PreparePayResult, PrepareTipResult, PurchaseDetails, RecoveryLoadRequest, RefreshReason, ReturnCoinsRequest, TestPayArgs, TrackDepositGroupRequest, TrackDepositGroupResponse, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, } from "./types/walletTypes"; import { AmountJson, Amounts } from "./util/amounts"; import { assertUnreachable } from "./util/assertUnreachable"; import { AsyncOpMemoSingle } from "./util/asyncMemo"; import { HttpRequestLibrary } from "./util/http"; import { Logger } from "./util/logging"; import { AsyncCondition } from "./util/promiseUtils"; import { Database } from "./util/query"; import { Duration, durationMin } from "./util/time"; import { TimerGroup } from "./util/timer"; const builtinCurrencies: CurrencyRecord[] = [ { auditors: [ { auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0", baseUrl: "https://auditor.demo.taler.net/", expirationStamp: new Date(2027, 1).getTime(), }, ], exchanges: [], fractionalDigits: 2, name: "KUDOS", }, ]; 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(): Database { return this.ws.db; } constructor( db: Database, http: HttpRequestLibrary, cryptoWorkerFactory: CryptoWorkerFactory, ) { this.ws = new InternalWalletState(db, http, cryptoWorkerFactory); } getExchangePaytoUri( exchangeBaseUrl: string, supportedTargetTypes: string[], ): Promise { return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes); } async getWithdrawalDetailsForAmount( exchangeBaseUrl: string, amount: AmountJson, ): Promise { const wi = await getExchangeWithdrawalInfo( this.ws, exchangeBaseUrl, amount, ); const paytoUris = wi.exchangeInfo.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, }; } 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.Bug: // Nothing to do, will just be displayed to the user return; 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, ); break; case PendingOperationType.ProposalChoice: // Nothing to do, user needs to accept/reject break; case PendingOperationType.ProposalDownload: await processDownloadProposal(this.ws, pending.proposalId, forceNow); break; case PendingOperationType.TipChoice: // Nothing to do, user needs to accept/reject 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); } } /** * Process pending operations. */ public async runPending(forceNow = false): Promise { const onlyDue = !forceNow; const pendingOpsResponse = await this.getPendingOperations({ onlyDue }); for (const p of pendingOpsResponse.pendingOperations) { 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); } } } } /** * 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({ onlyDue: false }) .then((pending) => { for (const p of pending.pendingOperations) { if (p.retryInfo && p.retryInfo.retryCounter > maxRetries) { console.warn( `stopping, as ${maxRetries} retries are exceeded in an operation of type ${p.type}`, ); this.stop(); done = true; resolve(); } } }) .catch((e) => { logger.error(e); reject(e); }); }); // Run this asynchronously this.runRetryLoop().catch((e) => { logger.error("exception in wallet retry loop"); reject(e); }); }); 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; } }); } private async runRetryLoopImpl(): Promise { let iteration = 0; for (; !this.stopped; iteration++) { const pending = await this.getPendingOperations({ onlyDue: true }); let numDueAndLive = 0; for (const p of pending.pendingOperations) { if (p.givesLifeness) { numDueAndLive++; } } // Make sure that we run tasks that don't give lifeness at least // one time. if (iteration !== 0 && numDueAndLive === 0) { const allPending = await this.getPendingOperations({ onlyDue: false }); let numPending = 0; let numGivingLiveness = 0; for (const p of allPending.pendingOperations) { numPending++; if (p.givesLifeness) { numGivingLiveness++; } } let dt: Duration; if ( allPending.pendingOperations.length === 0 || allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER ) { // Wait for 5 seconds dt = { d_ms: 5000 }; } else { dt = durationMin({ d_ms: 5000 }, allPending.nextRetryDelay); } const timeout = this.timerGroup.resolveAfter(dt); this.ws.notify({ type: NotificationType.WaitingForRetry, numGivingLiveness, numPending, }); await Promise.race([timeout, this.latch.wait()]); } else { // FIXME: maybe be a bit smarter about executing these // operations in parallel? logger.trace( `running ${pending.pendingOperations.length} pending operations`, ); for (const p of pending.pendingOperations) { 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, }); } } this.ws.notify({ type: NotificationType.PendingOperationProcessed, }); } } } 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.runWithWriteTransaction( [Stores.config, Stores.currencies], async (tx) => { let applied = false; await tx.iter(Stores.config).forEach((x) => { if (x.key == "currencyDefaultsApplied" && x.value == true) { applied = true; } }); if (!applied) { for (const c of builtinCurrencies) { await tx.put(Stores.currencies, 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); } /** * 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(); } } /** * First fetch information requred 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(); } } /** * 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.runWithReadTransaction( [Stores.exchanges, Stores.reserves], (tx) => getFundingPaytoUris(tx, resp.reservePub), ); return { reservePub: resp.reservePub, exchangePaytoUris, }; } finally { this.latch.trigger(); } } /** * 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 getWithdrawalDetailsForUri( talerWithdrawUri: string, ): Promise { return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri); } /** * Update or add exchange DB entry by fetching the /keys and /wire information. * Optionally link the reserve entry to the new or existing * exchange entry in then DB. */ async updateExchangeFromUrl( baseUrl: string, force = false, ): Promise { try { return updateExchangeFromUrl(this.ws, baseUrl, force); } finally { this.latch.trigger(); } } async getExchangeTos(exchangeBaseUrl: string): Promise { const exchange = await this.updateExchangeFromUrl(exchangeBaseUrl); const tos = exchange.termsOfServiceText; const currentEtag = exchange.termsOfServiceLastEtag; if (!tos || !currentEtag) { throw Error("exchange is in invalid state"); } return { acceptedEtag: exchange.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.runWithWriteTransaction( [Stores.refreshGroups, Stores.denominations, Stores.coins], async (tx) => { return await createRefreshGroup( this.ws, tx, [{ coinPub: oldCoinPub }], RefreshReason.Manual, ); }, ); await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId); } catch (e) { this.latch.trigger(); } } async findExchange( exchangeBaseUrl: string, ): Promise { return await this.db.get(Stores.exchanges, exchangeBaseUrl); } async getPendingOperations({ onlyDue = false, } = {}): Promise { return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws, { onlyDue }), ); } async acceptExchangeTermsOfService( exchangeBaseUrl: string, etag: string | undefined, ): Promise { return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag); } async getDenoms(exchangeUrl: string): Promise { const denoms = await this.db .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl) .toArray(); return denoms; } /** * Get all exchanges known to the exchange. * * @deprecated Use getExchanges instead */ async getExchangeRecords(): Promise { return await this.db.iter(Stores.exchanges).toArray(); } async getExchanges(): Promise { const exchanges: (ExchangeListItem | undefined)[] = await this.db .iter(Stores.exchanges) .map((x) => { const details = x.details; if (!details) { return undefined; } if (!x.addComplete) { return undefined; } if (!x.wireInfo) { return undefined; } return { exchangeBaseUrl: x.baseUrl, currency: details.currency, paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri), }; }); return { exchanges: exchanges.filter((x) => !!x) as ExchangeListItem[], }; } async getCurrencies(): Promise { return await this.db.iter(Stores.currencies).toArray(); } async updateCurrency(currencyRecord: CurrencyRecord): Promise { logger.trace("updating currency to", currencyRecord); await this.db.put(Stores.currencies, currencyRecord); } async getReserves(exchangeBaseUrl?: string): Promise { if (exchangeBaseUrl) { return await this.db .iter(Stores.reserves) .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl); } else { return await this.db.iter(Stores.reserves).toArray(); } } async getCoinsForExchange(exchangeBaseUrl: string): Promise { return await this.db .iter(Stores.coins) .filter((c) => c.exchangeBaseUrl === exchangeBaseUrl); } async getCoins(): Promise { return await this.db.iter(Stores.coins).toArray(); } /** * 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 getPurchase( contractTermsHash: string, ): Promise { return this.db.get(Stores.purchases, contractTermsHash); } async acceptTip(talerTipUri: string): Promise { try { return acceptTip(this.ws, talerTipUri); } catch (e) { this.latch.trigger(); } } 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.db.iter(Stores.reserves).toArray(); for (const r of reserves) { if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) { try { this.processReserve(r.reservePub); } catch (e) { console.error(e); } } } } /** * 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(); } } async updateReserve(reservePub: string): Promise { await forceQueryReserve(this.ws, reservePub); return await this.ws.db.get(Stores.reserves, reservePub); } async getReserve(reservePub: string): Promise { return await this.ws.db.get(Stores.reserves, reservePub); } async refuseProposal(proposalId: string): Promise { return refuseProposal(this.ws, proposalId); } async getPurchaseDetails(proposalId: string): Promise { const purchase = await this.db.get(Stores.purchases, proposalId); if (!purchase) { throw Error("unknown purchase"); } const refundsDoneAmounts = Object.values(purchase.refunds) .filter((x) => x.type === RefundState.Applied) .map((x) => x.refundAmount); const refundsPendingAmounts = Object.values(purchase.refunds) .filter((x) => x.type === RefundState.Pending) .map((x) => x.refundAmount); const totalRefundAmount = Amounts.sum([ ...refundsDoneAmounts, ...refundsPendingAmounts, ]).amount; const refundsDoneFees = Object.values(purchase.refunds) .filter((x) => x.type === RefundState.Applied) .map((x) => x.refundFee); const refundsPendingFees = Object.values(purchase.refunds) .filter((x) => x.type === RefundState.Pending) .map((x) => x.refundFee); const totalRefundFees = Amounts.sum([ ...refundsDoneFees, ...refundsPendingFees, ]).amount; const totalFees = totalRefundFees; return { contractTerms: JSON.parse(purchase.download.contractTermsRaw), hasRefund: purchase.timestampLastRefundStatus !== undefined, totalRefundAmount: totalRefundAmount, totalRefundAndRefreshFees: totalFees, }; } benchmarkCrypto(repetitions: number): Promise { return this.ws.cryptoApi.benchmark(repetitions); } async setCoinSuspended(coinPub: string, suspended: boolean): Promise { await this.db.runWithWriteTransaction([Stores.coins], async (tx) => { const c = await tx.get(Stores.coins, coinPub); if (!c) { logger.warn(`coin ${coinPub} not found, won't suspend`); return; } c.suspended = suspended; await tx.put(Stores.coins, c); }); } /** * Dump the public information of coins we have in an easy-to-process format. */ async dumpCoins(): Promise { const coins = await this.db.iter(Stores.coins).toArray(); const coinsJson: CoinDumpJson = { coins: [] }; for (const c of coins) { const denom = await this.db.get(Stores.denominations, [ 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 this.db.get( Stores.withdrawalGroups, 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 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/", }); 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.runWithWriteTransaction( [Stores.refreshGroups, Stores.denominations, Stores.coins], 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 "createDepositGroup": { const req = codecForCreateDepositGroupRequest().decode(payload); return await createDepositGroup(this.ws, req); } case "trackDepositGroup": const req = codecForTrackDepositGroupRequest().decode(payload); return trackDepositGroup(this.ws, req); } 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); return { type: "response", operation, id, result, }; } 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}`, {}, ), }; } } } }