diff options
author | Florian Dold <florian@dold.me> | 2021-06-15 18:52:43 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-06-15 18:52:43 +0200 |
commit | d41ae5eb97a5264b1d61321354eac049ca317c97 (patch) | |
tree | a8e78bee0ff2a92f0b3f1cb9230442186dd17358 /packages/taler-wallet-core/src/wallet.ts | |
parent | 4b16d7bd342dbb5376fd2cef08b14ebabbe4ed10 (diff) | |
download | wallet-core-d41ae5eb97a5264b1d61321354eac049ca317c97.tar.xz |
separate wallet state from wallet client
Diffstat (limited to 'packages/taler-wallet-core/src/wallet.ts')
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 1929 |
1 files changed, 919 insertions, 1010 deletions
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<void>(); - - get db(): DbAccess<typeof WalletStoresV1> { - return this.ws.db; - } - - constructor( - db: DbAccess<typeof WalletStoresV1>, - http: HttpRequestLibrary, - cryptoWorkerFactory: CryptoWorkerFactory, - ) { - this.ws = new InternalWalletState(db, http, cryptoWorkerFactory); +async function getWithdrawalDetailsForAmount( + ws: InternalWalletState, + exchangeBaseUrl: string, + amount: AmountJson, +): Promise<ManualWithdrawalDetails> { + 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<string> { - 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<void> { + 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<ManualWithdrawalDetails> { - 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<void> { + 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<void> { - 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<void> { - 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<void> { + let done = false; + const p = new Promise<void>((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<void> { - let done = false; - const p = new Promise<void>((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<void> { - // 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<void> { - 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<void> { + // 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<void> { + 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<void> { - 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<void> { + 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<PreparePayResult> { - 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<AcceptManualWithdrawalResult> { + 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<ConfirmPayResult> { - try { - return await confirmPay(this.ws, proposalId, sessionIdOverride); - } finally { - this.latch.trigger(); - } +async function getExchangeTos( + ws: InternalWalletState, + exchangeBaseUrl: string, +): Promise<GetExchangeTosResult> { + 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<void> { - try { - return await processReserve(this.ws, reservePub); - } finally { - this.latch.trigger(); - } +async function getExchanges( + ws: InternalWalletState, +): Promise<ExchangesListRespose> { + 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<AcceptWithdrawalResponse> { + 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<AcceptManualWithdrawalResult> { - 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<void> { + 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<void> { + 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<WithdrawUriInfoResponse> { - 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<CoinDumpJson> { + 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<void> { - 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<void> { - 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<Op extends WalletApiOperation & keyof WalletOperations>( + operation: Op, + payload: RequestType<Op>, + ): Promise<ResponseType<Op>>; +} - /** - * 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<WalletCoreApiClient> { + let id = 0; + const client: WalletCoreApiClient = { + async call(op, payload): Promise<any> { + 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<GetExchangeTosResult> { - const { exchange, exchangeDetails } = await this.updateExchangeFromUrl( - exchangeBaseUrl, +/** + * Implementation of the "wallet-core" API. + */ +async function dispatchRequestInternal( + ws: InternalWalletState, + operation: string, + payload: unknown, +): Promise<Record<string, any>> { + 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<BalancesResponse> { - return this.ws.memoGetBalance.memo(() => getBalances(this.ws)); - } - - async refresh(oldCoinPub: string): Promise<void> { - 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<PendingOperationsResponse> { - return this.ws.memoGetPending.memo(() => getPendingOperations(this.ws)); - } - - async acceptExchangeTermsOfService( - exchangeBaseUrl: string, - etag: string | undefined, - ): Promise<void> { - return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag); - } - - async getExchanges(): Promise<ExchangesListRespose> { - 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<WalletCurrencyInfo> { - 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<void> { - 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<ApplyRefundResponse> { - return applyRefund(this.ws, talerRefundUri); - } - - async acceptTip(talerTipUri: string): Promise<void> { - 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<PrepareTipResult> { - return prepareTip(this.ws, talerTipUri); - } - - async abortFailedPayWithRefund(proposalId: string): Promise<void> { - 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<void> { - 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<void> { - // 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<AcceptWithdrawalResponse> { - try { - return createTalerWithdrawReserve( - this.ws, - talerWithdrawUri, - selectedExchange, - ); - } finally { - this.latch.trigger(); + case "exportBackupPlain": { + return exportBackup(ws); } - } - - async refuseProposal(proposalId: string): Promise<void> { - return refuseProposal(this.ws, proposalId); - } - - benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> { - return this.ws.cryptoApi.benchmark(repetitions); - } - - async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> { - 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<CoinDumpJson> { - 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<TransactionsResponse> { - return getTransactions(this.ws, request); - } - - async withdrawTestBalance(req: WithdrawTestBalanceRequest): Promise<void> { - await withdrawTestBalance( - this.ws, - req.amount, - req.bankBaseUrl, - req.exchangeBaseUrl, - ); - } - - async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> { - 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<CoinRecord[]> { - return await this.db - .mktx((x) => ({ - coins: x.coins, - })) - .runReadOnly(async (tx) => { - return tx.coins.iter().toArray(); - }); - } - - async getReservesForExchange( - exchangeBaseUrl?: string, - ): Promise<ReserveRecord[]> { - 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<ReserveRecord | undefined> { - return await this.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(reservePub); - }); - } - - async runIntegrationtest(args: IntegrationTestArgs): Promise<void> { - 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<BackupRecovery> { - return getBackupRecovery(this.ws); - } - - async loadBackupRecovery(req: RecoveryLoadRequest): Promise<void> { - return loadBackupRecovery(this.ws, req); - } - - async addBackupProvider(req: AddBackupProviderRequest): Promise<void> { - return addBackupProvider(this.ws, req); - } - - async createDepositGroup( - req: CreateDepositGroupRequest, - ): Promise<CreateDepositGroupResponse> { - return createDepositGroup(this.ws, req); - } - - async runBackupCycle(): Promise<void> { - return runBackupCycle(this.ws); - } - - async getBackupStatus(): Promise<BackupInfo> { - return getBackupInfo(this.ws); - } - - async trackDepositGroup( - req: TrackDepositGroupRequest, - ): Promise<TrackDepositGroupResponse> { - return trackDepositGroup(this.ws, req); - } - - /** - * Implementation of the "wallet-core" API. - */ - private async dispatchRequestInternal( - operation: string, - payload: unknown, - ): Promise<Record<string, any>> { - 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<CoreApiResponse> { - 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<CoreApiResponse> { + 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}`, - {}, - ), - }; - } } } } |