taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 68ab975d0acb3adcd348d5dafb967686c9e7bf15
parent 1242672fd2ac96e783b891d7823b4e5880d2a99c
Author: Florian Dold <dold@taler.net>
Date:   Sun, 19 Jul 2026 23:12:12 +0200

wallet: move Cache to taler-util and API handlers to requests.ts

wallet.ts keeps the lifecycle, execution context and DB plumbing.

Diffstat:
Apackages/taler-util/src/cache.ts | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/index.ts | 1+
Mpackages/taler-wallet-core/src/dev-experiments.ts | 4++--
Apackages/taler-wallet-core/src/requests.ts | 2655+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/wallet.ts | 2948++++---------------------------------------------------------------------------
5 files changed, 2883 insertions(+), 2805 deletions(-)

diff --git a/packages/taler-util/src/cache.ts b/packages/taler-util/src/cache.ts @@ -0,0 +1,80 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * In-memory cache with a per-entry expiry and a capacity bound. + */ + +import { AbsoluteTime, Duration } from "./time.js"; + +export class Cache<T> { + private map: Map<string, [AbsoluteTime, T]> = new Map(); + + constructor( + private maxCapacity: number, + private cacheDuration: Duration, + ) {} + + get(key: string): T | undefined { + const r = this.map.get(key); + if (!r) { + return undefined; + } + + if (AbsoluteTime.isExpired(r[0])) { + this.map.delete(key); + return undefined; + } + + return r[1]; + } + + async getOrPut(key: string, lambda: () => Promise<T>): Promise<T>; + async getOrPut( + key: string, + lambda: () => Promise<T | undefined>, + ): Promise<T | undefined>; + async getOrPut( + key: string, + lambda: () => Promise<T | undefined>, + ): Promise<T | undefined> { + const cached = this.get(key); + if (cached != null) { + return cached; + } else { + const computed = await lambda(); + if (computed != null) { + this.put(key, computed); + } + return computed; + } + } + + clear(): void { + this.map.clear(); + } + + put(key: string, value: T): void { + if (this.map.size > this.maxCapacity) { + this.map.clear(); + } + const expiry = AbsoluteTime.addDuration( + AbsoluteTime.now(), + this.cacheDuration, + ); + this.map.set(key, [expiry, value]); + } +} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -4,6 +4,7 @@ export * from "./base64.js"; export * from "./bech32.js"; export * from "./bitcoin.js"; export * from "./CancellationToken.js"; +export * from "./cache.js"; export * from "./codec.js"; export * from "./contract-terms.js"; export * from "./errors.js"; diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -78,10 +78,10 @@ import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js"; import { RefreshTransactionContext } from "./refresh.js"; import { rematerializeTransactions } from "./transactions.js"; import { - WalletExecutionContext, handleAddGlobalCurrencyExchange, handleRemoveGlobalCurrencyExchange, -} from "./wallet.js"; +} from "./requests.js"; +import { WalletExecutionContext } from "./wallet.js"; import { WithdrawTransactionContext } from "./withdraw.js"; const logger = new Logger("dev-experiments.ts"); diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts @@ -0,0 +1,2655 @@ +/* + This file is part of GNU Taler + (C) 2015-2019 GNUnet e.V. + (C) 2025 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * High-level wallet operations that should be independent from the underlying + * browser extension interface. + */ + +/** + * Imports. + */ +import { + AbortTransactionRequest, + AcceptBankIntegratedWithdrawalRequest, + AcceptManualWithdrawalRequest, + AcceptManualWithdrawalResult, + AcceptWithdrawalResponse, + ActiveTask, + AddBankAccountRequest, + AddBankAccountResponse, + AddExchangeRequest, + AddExchangeResponse, + AddGlobalCurrencyAuditorRequest, + AddGlobalCurrencyExchangeRequest, + AmountString, + Amounts, + CanonicalizeBaseUrlRequest, + CanonicalizeBaseUrlResponse, + Codec, + CoinDumpJson, + CoinStatus, + CompleteBaseUrlRequest, + CompleteBaseUrlResult, + ConfirmPayRequest, + ConfirmPayResult, + ConvertIbanAccountFieldToPaytoRequest, + ConvertIbanAccountFieldToPaytoResponse, + ConvertIbanPaytoToAccountFieldRequest, + ConvertIbanPaytoToAccountFieldResponse, + CreateStoredBackupResponse, + DeleteDiscountRequest, + DeleteExchangeRequest, + DeleteStoredBackupRequest, + DeleteSubscriptionRequest, + EmptyObject, + ExchangeEntryStatus, + ExportDbToFileRequest, + ExportDbToFileResponse, + FailTransactionRequest, + ForgetBankAccountRequest, + GetActiveTasksResponse, + GetBankAccountByIdRequest, + GetBankAccountByIdResponse, + GetBankingChoicesForPaytoRequest, + GetBankingChoicesForPaytoResponse, + GetChoicesForPaymentRequest, + GetChoicesForPaymentResult, + GetCurrencySpecificationRequest, + GetCurrencySpecificationResponse, + GetDefaultExchangesResponse, + GetDepositWireTypesForCurrencyRequest, + GetDepositWireTypesForCurrencyResponse, + GetDepositWireTypesRequest, + GetDepositWireTypesResponse, + GetExchangeTosRequest, + GetExchangeTosResult, + GetPerformanceStatsRequest, + GetPerformanceStatsResponse, + GetQrCodesForPaytoRequest, + GetQrCodesForPaytoResponse, + HintNetworkAvailabilityRequest, + HostPortPath, + ImportDbFromFileRequest, + ImportDbRequest, + InitRequest, + InitResponse, + IntegrationTestArgs, + IntegrationTestV2Args, + ListBankAccountsRequest, + ListBankAccountsResponse, + ListDiscountsRequest, + ListDiscountsResponse, + ListGlobalCurrencyAuditorsResponse, + ListGlobalCurrencyExchangesResponse, + ListSubscriptionsRequest, + ListSubscriptionsResponse, + Logger, + NotificationType, + PaytoType, + Paytos, + PerformanceTable, + PrepareWithdrawExchangeRequest, + PrepareWithdrawExchangeResponse, + RecoverStoredBackupRequest, + RemoveGlobalCurrencyAuditorRequest, + RemoveGlobalCurrencyExchangeRequest, + Result, + RunFixupRequest, + ScopeType, + SharePaymentRequest, + SharePaymentResult, + StartRefundQueryRequest, + StoredBackupList, + SuspendTransactionRequest, + TalerBankIntegrationHttpClient, + TalerError, + TalerErrorCode, + TalerProtocolTimestamp, + TalerUriAction, + TalerUris, + TestingCorruptWithdrawalCoinSelRequest, + TestingGetDenomStatsRequest, + TestingGetDenomStatsResponse, + TestingGetDiagnosticsResponse, + TestingGetFlightRecordsResponse, + TestingGetReserveHistoryRequest, + TestingSetTimetravelRequest, + TestingWaitBalanceRequest, + TestingWaitExchangeReadyRequest, + TransactionType, + TransactionsResponse, + UpdateExchangeEntryRequest, + ValidateIbanRequest, + ValidateIbanResponse, + WalletBankAccountInfo, + WalletCoreVersion, + WireTypeDetails, + WithdrawTestBalanceRequest, + canonicalizeBaseUrl, + checkDbInvariant, + codecForAbortTransaction, + codecForAcceptBankIntegratedWithdrawalRequest, + codecForAcceptExchangeTosRequest, + codecForAcceptManualWithdrawalRequest, + codecForAcceptPeerPullPaymentRequest, + codecForAddBankAccountRequest, + codecForAddContactRequest, + codecForAddExchangeRequest, + codecForAddGlobalCurrencyAuditorRequest, + codecForAddGlobalCurrencyExchangeRequest, + codecForAddMailboxMessageRequest, + codecForAny, + codecForApplyDevExperiment, + codecForCancelProgressToken, + codecForCanonicalizeBaseUrlRequest, + codecForCheckDepositRequest, + codecForCheckPayTemplateRequest, + codecForCheckPeerPullPaymentRequest, + codecForCheckPeerPushDebitRequest, + codecForCompleteBaseUrlRequest, + codecForConfirmPayRequest, + codecForConfirmPeerPushPaymentRequest, + codecForConfirmWithdrawalRequestRequest, + codecForConvertAmountRequest, + codecForConvertIbanAccountFieldToPaytoRequest, + codecForConvertIbanPaytoToAccountFieldRequest, + codecForCreateDepositGroupRequest, + codecForDeleteContactRequest, + codecForDeleteDiscountRequest, + codecForDeleteExchangeRequest, + codecForDeleteMailboxMessageRequest, + codecForDeleteStoredBackupRequest, + codecForDeleteSubscriptionRequest, + codecForDeleteTransactionRequest, + codecForEmptyObject, + codecForExportDbToFileRequest, + codecForFailTransactionRequest, + codecForForceRefreshRequest, + codecForForgetBankAccount, + codecForGetBalanceDetailRequest, + codecForGetBankAccountByIdRequest, + codecForGetBankingChoicesForPaytoRequest, + codecForGetChoicesForPaymentRequest, + codecForGetCurrencyInfoRequest, + codecForGetDepositWireTypesForCurrencyRequest, + codecForGetDepositWireTypesRequest, + codecForGetDonauStatementsRequest, + codecForGetExchangeEntryByUrlRequest, + codecForGetExchangeResourcesRequest, + codecForGetExchangeTosRequest, + codecForGetMaxDepositAmountRequest, + codecForGetMaxPeerPushDebitAmountRequest, + codecForGetPerformanceStatsRequest, + codecForGetQrCodesForPaytoRequest, + codecForGetTransactionsV2Request, + codecForGetWithdrawalDetailsForAmountRequest, + codecForGetWithdrawalDetailsForUri, + codecForHintNetworkAvailabilityRequest, + codecForImportDbFromFileRequest, + codecForImportDbRequest, + codecForInitRequest, + codecForInitiatePeerPullPaymentRequest, + codecForInitiatePeerPushDebitRequest, + codecForIntegrationTestArgs, + codecForIntegrationTestV2Args, + codecForListBankAccounts, + codecForListDiscountsRequest, + codecForListExchangesRequest, + codecForListSubscriptionsRequest, + codecForMailboxBaseUrl, + codecForMailboxConfiguration, + codecForPrepareBankIntegratedWithdrawalRequest, + codecForPreparePayRequest, + codecForPreparePayTemplateRequest, + codecForPreparePeerPullPaymentRequest, + codecForPreparePeerPushCreditRequest, + codecForPrepareRefundRequest, + codecForPrepareWithdrawExchangeRequest, + codecForRecoverStoredBackupRequest, + codecForRemoveGlobalCurrencyAuditorRequest, + codecForRemoveGlobalCurrencyExchangeRequest, + codecForResumeTransaction, + codecForRetryProgressTokenNowRequest, + codecForRetryTransactionRequest, + codecForRunFixupRequest, + codecForSendTalerUriMailboxMessageRequest, + codecForSetCoinSuspendedRequest, + codecForSetDonauRequest, + codecForSharePaymentRequest, + codecForStartExchangeWalletKycRequest, + codecForStartRefundQueryRequest, + codecForSuspendTransaction, + codecForTaldirLookupRequest, + codecForTaldirRegistrationCompletionRequest, + codecForTaldirRegistrationRequest, + codecForTestPayArgs, + codecForTestingCorruptWithdrawalCoinSelRequest, + codecForTestingGetDenomStatsRequest, + codecForTestingGetReserveHistoryRequest, + codecForTestingPlanMigrateExchangeBaseUrlRequest, + codecForTestingSetTimetravelRequest, + codecForTestingWaitBalanceRequest, + codecForTestingWaitExchangeReadyRequest, + codecForTestingWaitWalletKycRequest, + codecForTransactionByIdRequest, + codecForTransactionsRequest, + codecForUpdateExchangeEntryRequest, + codecForValidateIbanRequest, + codecForWithdrawTestBalance, + convertCHF_BBANtoIBAN, + convertHUF_BBANtoIBAN, + encodeCrock, + getErrorDetailFromException, + getQrCodesForPayto, + getRandomBytes, + j2s, + parseIban, + setDangerousTimetravel, + setGlobalLogLevelFromString, + validateIban, +} from "@gnu-taler/taler-util"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { getBalanceDetail, getBalances } from "./balance.js"; +import { + getMaxDepositAmount, + getMaxPeerPushDebitAmount, +} from "./coinSelection.js"; +import { cancelableFetch } from "./common.js"; +import { addContact, deleteContact, listContacts } from "./contacts.js"; +import { + ConfigRecordKey, + timestampAbsoluteFromDb, + timestampProtocolToDb, +} from "./db-common.js"; +import { + CoinSourceType, + clearDatabase, + exportDb, + importDb, + openStoredBackupsDatabase, + walletDbFixups, +} from "./db-indexeddb.js"; +import { + isCandidateWithdrawableDenomRec, + isWithdrawableDenom, +} from "./denominations.js"; +import { checkDepositGroup, createDepositGroup } from "./deposits.js"; +import { applyDevExperiment } from "./dev-experiments.js"; +import { + handleGetDonau, + handleGetDonauStatements, + handleSetDonau, +} from "./donau.js"; +import { + acceptExchangeTermsOfService, + deleteExchange, + fetchFreshExchange, + forgetExchangeTermsOfService, + getExchangeDetailedInfo, + getExchangeDetailsInTx, + getExchangeResources, + getExchangeTos, + handleStartExchangeWalletKyc, + handleTestingPlanMigrateExchangeBaseUrl, + handleTestingWaitExchangeState, + handleTestingWaitExchangeWalletKyc, + listExchanges, + lookupExchangeByUri, + markExchangeUsed, + resetExchangeRetries, + startUpdateExchangeEntry, + waitReadyExchange, +} from "./exchanges.js"; +import { convertDepositAmount } from "./instructedAmountConversion.js"; +import { + addMailboxMessage, + createNewMailbox, + deleteMailboxMessage, + getMailbox, + listMailboxMessages, + refreshMailbox, + sendTalerUriMessage, +} from "./mailbox.js"; +import { + confirmPay, + getChoicesForPayment, + preparePayForTemplate, + preparePayForTemplateV2, + preparePayForUri, + preparePayForUriV2, + sharePayment, + startQueryRefund, + startRefundQueryForUri, +} from "./pay-merchant.js"; +import { + checkPeerPullCredit, + initiatePeerPullPayment, +} from "./pay-peer-pull-credit.js"; +import { + confirmPeerPullDebit, + preparePeerPullDebit, +} from "./pay-peer-pull-debit.js"; +import { + confirmPeerPushCredit, + preparePeerPushCredit, +} from "./pay-peer-push-credit.js"; +import { + checkPeerPushDebit, + checkPeerPushDebitV2, + initiatePeerPushDebit, +} from "./pay-peer-push-debit.js"; +import { checkPayForTemplate } from "./pay-template.js"; +import { fillDefaults } from "./preset-exchanges.js"; +import { + handleCancelProgressToken, + handleRetryProgressTokenNow, + withMaybeProgressContext, +} from "./progress.js"; +import { forceRefresh } from "./refresh.js"; +import { convertTaskToTransactionId, getActiveTaskIds } from "./shepherd.js"; +import { + completeAliasRegistration, + lookupAlias, + registerAlias, +} from "./taldir.js"; +import { + WithdrawTestBalanceResult, + runIntegrationTest, + runIntegrationTest2, + testPay, + testingWaitBalance, + waitTasksDone, + waitTransactionState, + waitUntilAllTransactionsFinal, + waitUntilRefreshesDone, + withdrawTestBalance, +} from "./testing.js"; +import { + deleteDiscount, + deleteSubscription, + listDiscounts, + listSubscriptions, +} from "./tokenFamilies.js"; +import { + abortTransaction, + deleteTransaction, + failTransaction, + getTransactionById, + getTransactions, + getTransactionsV2, + parseTransactionIdentifier, + rematerializeTransactions, + restartAll as restartAllRunningTasks, + resumeTransaction, + retryAll, + retryTransaction, + suspendTransaction, +} from "./transactions.js"; +import { + WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION, + WALLET_COREBANK_API_PROTOCOL_VERSION, + WALLET_CORE_API_PROTOCOL_VERSION, + WALLET_EXCHANGE_PROTOCOL_VERSION, + WALLET_MERCHANT_PROTOCOL_VERSION, +} from "./versions.js"; +import { + WalletApiOperation, + WalletCoreRequestType, + WalletCoreResponseType, +} from "./wallet-api-types.js"; +import { + acceptBankIntegratedWithdrawal, + confirmWithdrawal, + createManualWithdrawal, + getWithdrawalDetailsForAmount, + getWithdrawalDetailsForUri, + prepareBankIntegratedWithdrawal, +} from "./withdraw.js"; + +/** + * Request handlers for the wallet-core API, and the table that dispatches to + * them. + * + * Split out of wallet.ts, which keeps the wallet lifecycle, the execution + * context and the database plumbing these handlers run on top of. + */ + +import { + applyRunConfigDefaults, + getDenomInfo, + migrateMaterializedTransactions, + WalletExecutionContext, +} from "./wallet.js"; + +const logger = new Logger("requests.ts"); + +/** + * List bank accounts known to the wallet from + * previous withdrawals. + */ +async function handleListBankAccounts( + wex: WalletExecutionContext, + req: ListBankAccountsRequest, +): Promise<ListBankAccountsResponse> { + const accounts: WalletBankAccountInfo[] = []; + const currency = req.currency; + await wex.runWalletDbTx(async (tx) => { + const knownAccounts = await tx.listBankAccounts(); + for (const r of knownAccounts) { + if (currency && r.currencies && !r.currencies.includes(currency)) { + continue; + } + const payto = Result.orUndefined(Paytos.fromString(r.paytoUri)); + if (payto) { + accounts.push({ + bankAccountId: r.bankAccountId, + paytoUri: r.paytoUri, + label: r.label, + kycCompleted: r.kycCompleted, + currencies: r.currencies, + }); + } + } + }); + return { accounts }; +} + +async function handleGetBankAccountById( + wex: WalletExecutionContext, + req: GetBankAccountByIdRequest, +): Promise<GetBankAccountByIdResponse> { + const acct = await wex.runWalletDbTx(async (tx) => { + return tx.getBankAccount(req.bankAccountId); + }); + if (!acct) { + throw Error(`bank account ${req.bankAccountId} not found`); + } + return acct; +} + +/** + * Remove a known bank account. + */ +async function forgetBankAccount( + wex: WalletExecutionContext, + bankAccountId: string, +): Promise<void> { + await wex.runWalletDbTx(async (tx) => { + const account = await tx.getBankAccount(bankAccountId); + if (!account) { + throw Error(`account not found: ${bankAccountId}`); + } + await tx.deleteBankAccount(account.bankAccountId); + }); + wex.ws.notify({ + type: NotificationType.BankAccountChange, + bankAccountId, + }); + return; +} + +async function setCoinSuspended( + wex: WalletExecutionContext, + coinPub: string, + suspended: boolean, +): Promise<void> { + await wex.runWalletDbTx(async (tx) => { + const c = await tx.getCoin(coinPub); + if (!c) { + logger.warn(`coin ${coinPub} not found, won't suspend`); + return; + } + const coinAvailability = await tx.getCoinAvailability( + c.exchangeBaseUrl, + c.denomPubHash, + c.maxAge, + ); + checkDbInvariant( + !!coinAvailability, + `no denom info for ${c.denomPubHash} age ${c.maxAge}`, + ); + if (suspended) { + if (c.status !== CoinStatus.Fresh) { + return; + } + if (coinAvailability.freshCoinCount === 0) { + throw Error( + `invalid coin count ${coinAvailability.freshCoinCount} in DB`, + ); + } + coinAvailability.freshCoinCount--; + c.status = CoinStatus.FreshSuspended; + } else { + if (c.status == CoinStatus.Dormant) { + return; + } + coinAvailability.freshCoinCount++; + c.status = CoinStatus.Fresh; + } + await tx.upsertCoin(c); + await tx.upsertCoinAvailability(coinAvailability); + }); +} + +/** + * Dump the public information of coins we have in an easy-to-process format. + */ +async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> { + const coinsJson: CoinDumpJson = { coins: [] }; + logger.info("dumping coins"); + await wex.runWalletDbTx(async (tx) => { + const coins = await tx.listAllCoins(); + for (const c of coins) { + const denom = await tx.getDenomination(c.exchangeBaseUrl, c.denomPubHash); + if (!denom) { + logger.warn("no denom 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) { + withdrawalReservePub = cs.reservePub; + } + const denomInfo = await getDenomInfo( + wex, + tx, + c.exchangeBaseUrl, + c.denomPubHash, + ); + if (!denomInfo) { + logger.warn("no denomination found for coin"); + continue; + } + const historyRec = await tx.getCoinHistory(c.coinPub); + coinsJson.coins.push({ + coinPub: c.coinPub, + denomPub: denom.denomPub, + denomPubHash: c.denomPubHash, + denomValue: denom.value, + exchangeBaseUrl: c.exchangeBaseUrl, + refreshParentCoinPub: refreshParentCoinPub, + withdrawalReservePub: withdrawalReservePub, + coinStatus: c.status, + ageCommitmentProof: c.ageCommitmentProof, + history: historyRec ? historyRec.history : [], + }); + } + }); + return coinsJson; +} + +async function createStoredBackup( + wex: WalletExecutionContext, +): Promise<CreateStoredBackupResponse> { + const backup = await exportDb(wex.ws.idbFactory); + const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); + const name = `backup-${new Date().getTime()}`; + await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { + await tx.backupMeta.add({ + name, + }); + await tx.backupData.add(backup, name); + }); + return { + name, + }; +} + +async function listStoredBackups( + wex: WalletExecutionContext, +): Promise<StoredBackupList> { + const storedBackups: StoredBackupList = { + storedBackups: [], + }; + const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); + await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { + await tx.backupMeta.iter().forEach((x) => { + storedBackups.storedBackups.push({ + name: x.name, + }); + }); + }); + return storedBackups; +} + +async function deleteStoredBackup( + wex: WalletExecutionContext, + req: DeleteStoredBackupRequest, +): Promise<void> { + const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); + await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { + await tx.backupData.delete(req.name); + await tx.backupMeta.delete(req.name); + }); +} + +async function recoverStoredBackup( + wex: WalletExecutionContext, + req: RecoverStoredBackupRequest, +): Promise<void> { + logger.info(`Recovering stored backup ${req.name}`); + const { name } = req; + const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); + const bd = await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { + const backupMeta = await tx.backupMeta.get(name); + if (!backupMeta) { + throw Error("backup not found"); + } + const backupData = await tx.backupData.get(name); + if (!backupData) { + throw Error("no backup data (DB corrupt)"); + } + return backupData; + }); + logger.info(`backup found, now importing`); + await importDb(wex.db.idbHandle(), bd); + await wex.runLegacyWalletDbTx(async (tx) => { + await rematerializeTransactions(wex, tx.wtx); + // Clear fixups. Okay since they are idempotent. + const fixups = await tx.fixups.getAll(); + for (const f of fixups) { + await tx.fixups.delete(f.fixupName); + } + }); + logger.info(`import done`); +} + +async function handlePrepareWithdrawExchange( + wex: WalletExecutionContext, + req: PrepareWithdrawExchangeRequest, +): Promise<PrepareWithdrawExchangeResponse> { + const parsedUri = Result.orUndefined(TalerUris.parse(req.talerUri)); + if (parsedUri?.type !== TalerUriAction.WithdrawExchange) { + throw Error("expected a taler://withdraw-exchange URI"); + } + const exchangeBaseUrl = parsedUri.exchangeBaseUrl; + + return await withMaybeProgressContext( + wex, + "prepareWithdrawExchange", + req.progressToken, + async (pc) => { + if (pc) { + pc.onRetryNow = async () => { + await resetExchangeRetries(wex, exchangeBaseUrl); + }; + } + const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, { + noBail: pc != null, + progressContext: pc, + }); + if (parsedUri.amount) { + const amt = Amounts.parseOrThrow(parsedUri.amount); + if (amt.currency !== exchange.currency) { + throw Error("mismatch of currency (URI vs exchange)"); + } + } + return { + exchangeBaseUrl, + amount: parsedUri.amount, + }; + }, + ); +} + +async function handleSharePayment( + wex: WalletExecutionContext, + req: SharePaymentRequest, +): Promise<SharePaymentResult> { + return await sharePayment(wex, req.merchantBaseUrl, req.orderId); +} + +async function handleDeleteStoredBackup( + wex: WalletExecutionContext, + req: DeleteStoredBackupRequest, +): Promise<EmptyObject> { + await deleteStoredBackup(wex, req); + return {}; +} + +async function handleRecoverStoredBackup( + wex: WalletExecutionContext, + req: RecoverStoredBackupRequest, +): Promise<EmptyObject> { + await recoverStoredBackup(wex, req); + return {}; +} + +const urlCharRegex = /^[a-zA-Z0-9\-_.~!*'();:@&=+$,/?%#[\]]+$/; + +export async function handleCompleteExchangeBaseUrl( + wex: WalletExecutionContext, + req: CompleteBaseUrlRequest, +): Promise<CompleteBaseUrlResult> { + const trimmedUrl = req.url.trim(); + + if (!urlCharRegex.test(trimmedUrl)) { + return { + status: "bad-syntax", + error: { + code: TalerErrorCode.WALLET_CORE_API_BAD_REQUEST, + }, + }; + } + + // FIXME: Do completion via network. + return { + completion: canonicalizeBaseUrl(trimmedUrl), + status: "ok", + }; +} + +async function handleSetWalletRunConfig( + wex: WalletExecutionContext, + req: InitRequest, +) { + if (logger.shouldLogTrace()) { + const initType = wex.ws.initCalled + ? "repeat initialization" + : "first initialization"; + logger.trace(`init request (${initType}): ${j2s(req)}`); + } + + // Write to the DB to make sure that we're failing early in + // case the DB is not writeable. + try { + await wex.runWalletDbTx(async (tx) => { + tx.upsertConfig({ + key: ConfigRecordKey.LastInitInfo, + value: timestampProtocolToDb(TalerProtocolTimestamp.now()), + }); + }); + } catch (e) { + logger.error( + "error writing to database during initialization (while writing last error info)", + ); + const err = getErrorDetailFromException(e); + logger.error(`details: ${j2s(err)}`); + throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, { + innerError: err, + }); + } + wex.ws.initWithConfig(applyRunConfigDefaults(req.config)); + + if (wex.ws.config.testing.skipDefaults) { + logger.trace("skipping defaults"); + } else { + logger.trace("filling defaults"); + await fillDefaults(wex); + } + + if (req.config?.logLevel) { + setGlobalLogLevelFromString(req.config.logLevel); + } + + await migrateMaterializedTransactions(wex); + + const resp: InitResponse = { + versionInfo: await handleGetVersion(wex), + }; + + if (req.config?.lazyTaskLoop) { + logger.trace("lazily starting task loop"); + } else { + await wex.taskScheduler.ensureRunning(); + } + + wex.ws.initCalled = true; + return resp; +} + +async function handleWithdrawTestkudos(wex: WalletExecutionContext) { + return await withdrawTestBalance(wex, { + amount: "TESTKUDOS:10" as AmountString, + corebankApiBaseUrl: "https://bank.test.taler.net/", + exchangeBaseUrl: "https://exchange.test.taler.net/", + }); +} + +async function handleWithdrawTestBalance( + wex: WalletExecutionContext, + req: WithdrawTestBalanceRequest, +): Promise<WithdrawTestBalanceResult> { + return await withdrawTestBalance(wex, req); +} + +async function handleRunIntegrationTest( + wex: WalletExecutionContext, + req: IntegrationTestArgs, +): Promise<EmptyObject> { + await runIntegrationTest(wex, req); + return {}; +} + +async function handleRunIntegrationTestV2( + wex: WalletExecutionContext, + req: IntegrationTestV2Args, +): Promise<EmptyObject> { + await runIntegrationTest2(wex, req); + return {}; +} + +async function handleValidateIban( + wex: WalletExecutionContext, + req: ValidateIbanRequest, +): Promise<ValidateIbanResponse> { + const valRes = validateIban(req.iban); + const resp: ValidateIbanResponse = { + valid: valRes.type === "valid", + }; + return resp; +} + +async function handleAddExchange( + wex: WalletExecutionContext, + req: AddExchangeRequest, +): Promise<AddExchangeResponse> { + let exchangeBaseUrl: string; + if (req.exchangeBaseUrl) { + logger.warn( + "Deprecated request property: AddExchangeRequest.exchangeBaseUrl", + ); + exchangeBaseUrl = req.exchangeBaseUrl; + } else if (req.uri) { + if (req.uri.startsWith("taler")) { + const p = Result.orUndefined(TalerUris.parse(req.uri)); + if (p?.type !== TalerUriAction.AddExchange) { + throw Error("invalid taler://add-exchange/ URI"); + } + exchangeBaseUrl = p.exchangeBaseUrl; + } else if (req.allowCompletion) { + const completeRes = await handleCompleteExchangeBaseUrl(wex, { + url: req.uri, + }); + if (completeRes.status != "ok") { + throw TalerError.fromUncheckedDetail(completeRes.error); + } + exchangeBaseUrl = completeRes.completion; + } else if (req.uri.startsWith("http")) { + const canonUrl = canonicalizeBaseUrl(req.uri); + if (req.uri != canonUrl) { + throw Error("exchange base URL must be canonicalized"); + } + exchangeBaseUrl = req.uri; + } else { + throw Error("AddExchangeRequest.uri must be http(s):// or taler://"); + } + } else { + throw Error( + "AddExchangeRequest must either specify uri or exchangeBaseUrl", + ); + } + + // FIXME: Check /config before adding the exchange entry. + + // FIXME: We probably should not wait synchronously here. + await fetchFreshExchange(wex, exchangeBaseUrl, {}); + // Exchange has been explicitly added upon user request. + // Thus, we mark it as "used". + if (!req.ephemeral) { + await wex.runWalletDbTx(async (tx) => { + await markExchangeUsed(tx, exchangeBaseUrl); + }); + } + return { + exchangeBaseUrl, + }; +} + +async function handleUpdateExchangeEntry( + wex: WalletExecutionContext, + req: UpdateExchangeEntryRequest, +): Promise<EmptyObject> { + await startUpdateExchangeEntry(wex, req.exchangeBaseUrl, { + forceUpdate: !!req.force, + forceUnavailable: !!req.force, + }); + return {}; +} + +async function handleTestingGetDenomStats( + wex: WalletExecutionContext, + req: TestingGetDenomStatsRequest, +): Promise<TestingGetDenomStatsResponse> { + const denomStats: TestingGetDenomStatsResponse = { + numKnown: 0, + numLost: 0, + numOffered: 0, + }; + await wex.runWalletDbTx(async (tx) => { + const denoms = await tx.getDenominationsByExchange(req.exchangeBaseUrl); + for (const d of denoms) { + denomStats.numKnown++; + if (d.isOffered) { + denomStats.numOffered++; + } + if (d.isLost) { + denomStats.numLost++; + } + } + }); + return denomStats; +} + +async function handleAddBankAccount( + wex: WalletExecutionContext, + req: AddBankAccountRequest, +): Promise<AddBankAccountResponse> { + if (Result.isError(Paytos.fromString(req.paytoUri))) { + throw Error("invalid payto"); + } + const acctId = await wex.runWalletDbTx(async (tx) => { + let currencies = req.currencies; + let myId: string; + const oldAcct = await tx.getBankAccountByPaytoUri(req.paytoUri); + if (req.replaceBankAccountId) { + myId = req.replaceBankAccountId; + } else if (oldAcct) { + myId = oldAcct.bankAccountId; + currencies = [ + ...new Set([...(req.currencies ?? []), ...(oldAcct.currencies ?? [])]), + ]; + currencies.sort(); + } else { + // New Account! + myId = `acct:${encodeCrock(getRandomBytes(32))}`; + } + await tx.upsertBankAccount({ + bankAccountId: myId, + paytoUri: req.paytoUri, + label: req.label, + currencies, + kycCompleted: false, + }); + return myId; + }); + wex.ws.notify({ + type: NotificationType.BankAccountChange, + bankAccountId: acctId, + }); + return { + bankAccountId: acctId, + }; +} + +async function handleForgetBankAccount( + wex: WalletExecutionContext, + req: ForgetBankAccountRequest, +): Promise<EmptyObject> { + await forgetBankAccount(wex, req.bankAccountId); + return {}; +} + +// FIXME: Doesn't have proper type! +async function handleTestingGetReserveHistory( + wex: WalletExecutionContext, + req: TestingGetReserveHistoryRequest, +): Promise<any> { + const reserve = await wex.runWalletDbTx(async (tx) => { + return tx.getReserveByReservePub(req.reservePub); + }); + if (!reserve) { + throw Error("no reserve pub found"); + } + const sigResp = await wex.cryptoApi.signReserveHistoryReq({ + reservePriv: reserve.reservePriv, + startOffset: 0, + }); + const exchangeBaseUrl = req.exchangeBaseUrl; + const url = new URL(`reserves/${req.reservePub}/history`, exchangeBaseUrl); + const resp = await cancelableFetch(wex, url, { + headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, + }); + const historyJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); + return historyJson; +} + +async function handleAcceptManualWithdrawal( + wex: WalletExecutionContext, + req: AcceptManualWithdrawalRequest, +): Promise<AcceptManualWithdrawalResult> { + const res = await createManualWithdrawal(wex, { + amount: Amounts.parseOrThrow(req.amount), + exchangeBaseUrl: req.exchangeBaseUrl, + restrictAge: req.restrictAge, + forceReservePriv: req.forceReservePriv, + }); + return res; +} + +async function handleGetExchangeTos( + wex: WalletExecutionContext, + req: GetExchangeTosRequest, +): Promise<GetExchangeTosResult> { + return await withMaybeProgressContext( + wex, + "getExchangeTos", + req.progressToken, + async (pc) => { + return getExchangeTos( + wex, + req.exchangeBaseUrl, + req.acceptedFormat, + req.acceptLanguage, + pc, + ); + }, + ); +} + +async function handleGetQrCodesForPayto( + wex: WalletExecutionContext, + req: GetQrCodesForPaytoRequest, +): Promise<GetQrCodesForPaytoResponse> { + return { + codes: getQrCodesForPayto(req.paytoUri), + }; +} + +async function handleGetBankingChoicesForPayto( + wex: WalletExecutionContext, + req: GetBankingChoicesForPaytoRequest, +): Promise<GetBankingChoicesForPaytoResponse> { + const parsedPayto = Result.orUndefined(Paytos.fromString(req.paytoUri)); + if (!parsedPayto) { + throw Error("invalid payto URI"); + } + const amount = parsedPayto.params["amount"]; + if (!amount) { + logger.warn("payto URI has no amount"); + return { + choices: [], + }; + } + const currency = Amounts.currencyOf(amount); + switch (currency) { + case "KUDOS": + return { + choices: [ + { + label: "Demobank Website", + type: "link", + uri: `https://bank.demo.taler.net/webui/#/transfer/${encodeURIComponent( + req.paytoUri, + )}`, + }, + { + label: "Demobank App", + type: "link", + uri: `https://bank.demo.taler.net/app/transfer/${encodeURIComponent( + req.paytoUri, + )}`, + }, + ], + }; + break; + default: + return { + choices: [], + }; + } +} + +async function handleGetChoicesForPayment( + wex: WalletExecutionContext, + req: GetChoicesForPaymentRequest, +): Promise<GetChoicesForPaymentResult> { + return await getChoicesForPayment(wex, req.transactionId, req.forcedCoinSel); +} + +async function handleConfirmPay( + wex: WalletExecutionContext, + req: ConfirmPayRequest, +): Promise<ConfirmPayResult> { + return await confirmPay(wex, { + transactionId: req.transactionId, + choiceIndex: req.choiceIndex, + forcedCoinSel: undefined, + sessionIdOverride: req.sessionId, + useDonau: req.useDonau, + noWait: req.noWait ?? wex.ws.devExperimentState.flagConfirmPayNoWait, + }); +} + +async function handleListDiscounts( + wex: WalletExecutionContext, + req: ListDiscountsRequest, +): Promise<ListDiscountsResponse> { + return await listDiscounts(wex, req.tokenIssuePubHash, req.merchantBaseUrl); +} + +async function handleDeleteDiscount( + wex: WalletExecutionContext, + req: DeleteDiscountRequest, +): Promise<EmptyObject> { + return await deleteDiscount(wex, req.tokenFamilyHash); +} + +async function handleListSubscriptions( + wex: WalletExecutionContext, + req: ListSubscriptionsRequest, +): Promise<ListSubscriptionsResponse> { + return await listSubscriptions( + wex, + req.tokenIssuePubHash, + req.merchantBaseUrl, + ); +} + +async function handleDeleteSubscription( + wex: WalletExecutionContext, + req: DeleteSubscriptionRequest, +): Promise<EmptyObject> { + return await deleteSubscription(wex, req.tokenFamilyHash); +} + +async function handleAbortTransaction( + wex: WalletExecutionContext, + req: AbortTransactionRequest, +): Promise<EmptyObject> { + await abortTransaction(wex, req.transactionId); + return {}; +} + +async function handleSuspendTransaction( + wex: WalletExecutionContext, + req: SuspendTransactionRequest, +): Promise<EmptyObject> { + await suspendTransaction(wex, req.transactionId); + return {}; +} + +async function handleGetActiveTasks( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<GetActiveTasksResponse> { + const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds; + + const tasksInfo = await Promise.all( + allTasksId.map(async (id) => { + return await wex.runWalletDbTx(async (tx) => { + return tx.getOperationRetry(id); + }); + }), + ); + + const tasks = allTasksId.map((taskId, i): ActiveTask => { + const transaction = convertTaskToTransactionId(taskId); + const d = tasksInfo[i]; + + const firstTry = !d + ? undefined + : timestampAbsoluteFromDb(d.retryInfo.firstTry); + const nextTry = !d + ? undefined + : timestampAbsoluteFromDb(d.retryInfo.nextRetry); + const counter = d?.retryInfo.retryCounter; + const lastError = d?.lastError; + + return { + taskId: taskId, + retryCounter: counter, + firstTry, + nextTry, + lastError, + transaction, + }; + }); + return { tasks }; +} + +async function handleFailTransaction( + wex: WalletExecutionContext, + req: FailTransactionRequest, +): Promise<EmptyObject> { + await failTransaction(wex, req.transactionId); + return {}; +} + +async function handleTestingGetSampleTransactions( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<TransactionsResponse> { + // FIXME! + return { transactions: [] }; + // These are out of date! + //return { transactions: sampleWalletCoreTransactions }; +} + +async function handleStartRefundQuery( + wex: WalletExecutionContext, + req: StartRefundQueryRequest, +): Promise<EmptyObject> { + const txIdParsed = parseTransactionIdentifier(req.transactionId); + if (!txIdParsed) { + throw Error("invalid transaction ID"); + } + if (txIdParsed.tag !== TransactionType.Payment) { + throw Error("expected payment transaction ID"); + } + await startQueryRefund(wex, txIdParsed.proposalId); + return {}; +} + +async function handleHintNetworkAvailability( + wex: WalletExecutionContext, + req: HintNetworkAvailabilityRequest, +): Promise<EmptyObject> { + // If network was already available, don't do anything + if (wex.ws.networkAvailable === req.isNetworkAvailable) { + return {}; + } + wex.ws.networkAvailable = req.isNetworkAvailable; + // When network becomes available, restart tasks as they're blocked + // waiting for the network. + // When network goes down, restart tasks so they notice the network + // is down and wait. + await restartAllRunningTasks(wex); + return {}; +} + +async function handleGetDepositWireTypes( + wex: WalletExecutionContext, + req: GetDepositWireTypesRequest, +): Promise<GetDepositWireTypesResponse> { + const wtSet: Set<string> = new Set(); + const wireTypeDetails: WireTypeDetails[] = []; + const talerBankHostnames: string[] = []; + await wex.runWalletDbTx(async (tx) => { + const exchanges = await tx.getExchanges(); + for (const exchange of exchanges) { + const det = await getExchangeDetailsInTx(tx, exchange.baseUrl); + if (!det) { + continue; + } + if (req.currency !== undefined && det.currency !== req.currency) { + continue; + } + for (const acc of det.wireInfo.accounts) { + let usable = true; + for (const dr of acc.debit_restrictions) { + if (dr.type === "deny") { + usable = false; + break; + } + } + if (!usable) { + continue; + } + const parsedPayto = Result.orUndefined( + Paytos.fromString(acc.payto_uri), + ); + if (!parsedPayto) { + continue; + } + let preferredEntryType: "iban" | "bban" | undefined = undefined; + if (parsedPayto.targetType === PaytoType.IBAN) { + if (det.currency === "HUF") { + preferredEntryType = "bban"; + } else { + preferredEntryType = "iban"; + } + } + if (parsedPayto.targetType === PaytoType.TalerBank) { + if (!talerBankHostnames.includes(parsedPayto.host)) { + talerBankHostnames.push(parsedPayto.host); + } + } + if (!wtSet.has(parsedPayto.targetType!)) { + wtSet.add(parsedPayto.targetType!); + wireTypeDetails.push({ + paymentTargetType: parsedPayto.targetType!, + // Will possibly extended later by other exchanges + // with the same wire type. + talerBankHostnames, + preferredEntryType, + }); + } + } + } + }); + return { + wireTypeDetails, + }; +} + +async function handleGetDepositWireTypesForCurrency( + wex: WalletExecutionContext, + req: GetDepositWireTypesForCurrencyRequest, +): Promise<GetDepositWireTypesForCurrencyResponse> { + const wtSet: Set<string> = new Set(); + const wireTypeDetails: WireTypeDetails[] = []; + const talerBankHostnames: string[] = []; + await wex.runWalletDbTx(async (tx) => { + const exchanges = await tx.getExchanges(); + for (const exchange of exchanges) { + const det = await getExchangeDetailsInTx(tx, exchange.baseUrl); + if (!det) { + continue; + } + if (det.currency !== req.currency) { + continue; + } + for (const acc of det.wireInfo.accounts) { + let usable = true; + for (const dr of acc.debit_restrictions) { + if (dr.type === "deny") { + usable = false; + break; + } + } + if (!usable) { + continue; + } + const parsedPayto = Result.orUndefined( + Paytos.fromString(acc.payto_uri), + ); + if (!parsedPayto) { + continue; + } + if (parsedPayto.targetType === PaytoType.TalerBank) { + if (!talerBankHostnames.includes(parsedPayto.host)) { + talerBankHostnames.push(parsedPayto.host); + } + } + if (!wtSet.has(parsedPayto.targetType!)) { + wtSet.add(parsedPayto.targetType!); + wireTypeDetails.push({ + paymentTargetType: parsedPayto.targetType!, + // Will possibly extended later by other exchanges + // with the same wire type. + talerBankHostnames, + }); + } + } + } + }); + return { + wireTypes: [...wtSet], + wireTypeDetails, + }; +} + +async function handleListGlobalCurrencyExchanges( + wex: WalletExecutionContext, + _req: EmptyObject, +): Promise<ListGlobalCurrencyExchangesResponse> { + const resp: ListGlobalCurrencyExchangesResponse = { + exchanges: [], + }; + await wex.runWalletDbTx(async (tx) => { + const gceList = await tx.listGlobalCurrencyExchanges(); + for (const gce of gceList) { + resp.exchanges.push({ + currency: gce.currency, + exchangeBaseUrl: gce.exchangeBaseUrl, + exchangeMasterPub: gce.exchangeMasterPub, + }); + } + }); + return resp; +} + +async function handleListGlobalCurrencyAuditors( + wex: WalletExecutionContext, + _req: EmptyObject, +): Promise<ListGlobalCurrencyAuditorsResponse> { + const resp: ListGlobalCurrencyAuditorsResponse = { + auditors: [], + }; + await wex.runWalletDbTx(async (tx) => { + const gcaList = await tx.listGlobalCurrencyAuditors(); + for (const gca of gcaList) { + resp.auditors.push({ + currency: gca.currency, + auditorBaseUrl: gca.auditorBaseUrl, + auditorPub: gca.auditorPub, + }); + } + }); + return resp; +} + +export async function handleAddGlobalCurrencyExchange( + wex: WalletExecutionContext, + req: AddGlobalCurrencyExchangeRequest, +): Promise<EmptyObject> { + await wex.runWalletDbTx(async (tx) => { + const existingRec = await tx.getGlobalCurrencyExchange( + req.currency, + req.exchangeBaseUrl, + req.exchangeMasterPub, + ); + if (existingRec) { + return; + } + wex.ws.exchangeCache.clear(); + // Re-scope currency info from exchange scope to global scope. The scope + // string is the primary key, so this writes a new record and leaves the + // exchange-scoped one, as it did before. + const infoRec = await tx.getCurrencyInfo({ + type: ScopeType.Exchange, + currency: req.currency, + url: req.exchangeBaseUrl, + }); + if (infoRec) { + await tx.upsertCurrencyInfo({ + scopeInfo: { type: ScopeType.Global, currency: req.currency }, + currencySpec: infoRec.currencySpec, + source: infoRec.source, + }); + } + await tx.addGlobalCurrencyExchange({ + currency: req.currency, + exchangeBaseUrl: req.exchangeBaseUrl, + exchangeMasterPub: req.exchangeMasterPub, + }); + }); + return {}; +} + +async function handleRemoveGlobalCurrencyAuditor( + wex: WalletExecutionContext, + req: RemoveGlobalCurrencyAuditorRequest, +): Promise<EmptyObject> { + await wex.runWalletDbTx(async (tx) => { + const existingRec = await tx.getGlobalCurrencyAuditor( + req.currency, + req.auditorBaseUrl, + req.auditorPub, + ); + if (!existingRec) { + return; + } + checkDbInvariant( + !!existingRec.id, + `no global currency for ${req.currency}`, + ); + await tx.deleteGlobalCurrencyAuditor(existingRec.id); + wex.ws.exchangeCache.clear(); + }); + return {}; +} + +export async function handleRemoveGlobalCurrencyExchange( + wex: WalletExecutionContext, + req: RemoveGlobalCurrencyExchangeRequest, +): Promise<EmptyObject> { + await wex.runWalletDbTx(async (tx) => { + const existingRec = await tx.getGlobalCurrencyExchange( + req.currency, + req.exchangeBaseUrl, + req.exchangeMasterPub, + ); + if (!existingRec) { + return; + } + wex.ws.exchangeCache.clear(); + checkDbInvariant( + !!existingRec.id, + `no global exchange for ${req.currency}`, + ); + await tx.deleteCurrencyInfo({ + type: ScopeType.Global, + currency: req.currency, + }); + await tx.deleteGlobalCurrencyExchange(existingRec.id); + }); + return {}; +} + +async function handleAddGlobalCurrencyAuditor( + wex: WalletExecutionContext, + req: AddGlobalCurrencyAuditorRequest, +): Promise<EmptyObject> { + await wex.runWalletDbTx(async (tx) => { + const existingRec = await tx.getGlobalCurrencyAuditor( + req.currency, + req.auditorBaseUrl, + req.auditorPub, + ); + if (existingRec) { + return; + } + await tx.addGlobalCurrencyAuditor({ + currency: req.currency, + auditorBaseUrl: req.auditorBaseUrl, + auditorPub: req.auditorPub, + }); + wex.ws.exchangeCache.clear(); + }); + return {}; +} + +async function handleShutdown( + wex: WalletExecutionContext, + _req: EmptyObject, +): Promise<EmptyObject> { + logger.info(`Shutdown requested`); + wex.ws.stop(); + return {}; +} + +async function handleTestingSetTimetravel( + wex: WalletExecutionContext, + req: TestingSetTimetravelRequest, +): Promise<EmptyObject> { + setDangerousTimetravel(req.offsetMs); + await wex.taskScheduler.reload(); + return {}; +} + +async function handleCanonicalizeBaseUrl( + _wex: WalletExecutionContext, + req: CanonicalizeBaseUrlRequest, +): Promise<CanonicalizeBaseUrlResponse> { + return { + url: canonicalizeBaseUrl(req.url), + }; +} + +async function handleDeleteExchange( + wex: WalletExecutionContext, + req: DeleteExchangeRequest, +): Promise<EmptyObject> { + await deleteExchange(wex, req); + return {}; +} + +async function handleCreateStoredBackup( + wex: WalletExecutionContext, + _req: EmptyObject, +): Promise<CreateStoredBackupResponse> { + return await createStoredBackup(wex); +} + +async function handleExportDbToFile( + wex: WalletExecutionContext, + req: ExportDbToFileRequest, +): Promise<ExportDbToFileResponse> { + const res = await wex.ws.dbImplementation.exportToFile( + req.directory, + req.stem, + req.forceFormat, + ); + return { + path: res.path, + }; +} + +async function handleImportDb( + wex: WalletExecutionContext, + req: ImportDbRequest, +): Promise<EmptyObject> { + // FIXME: This should atomically re-materialize transactions! + await importDb(wex.db.idbHandle(), req.dump); + await wex.runLegacyWalletDbTx(async (tx) => { + await rematerializeTransactions(wex, tx.wtx); + // Clear fixups. Okay since they are idempotent. + const fixups = await tx.fixups.getAll(); + for (const f of fixups) { + await tx.fixups.delete(f.fixupName); + } + }); + return {}; +} + +async function handleImportDbFromFile( + wex: WalletExecutionContext, + req: ImportDbFromFileRequest, +): Promise<EmptyObject> { + if (req.path.endsWith(".json")) { + const dump = await wex.ws.dbImplementation.readBackupJson(req.path); + return await handleImportDb(wex, { + dump, + }); + } else { + throw Error("DB file import only supports .json files at the moment"); + } +} + +async function handleAcceptBankIntegratedWithdrawal( + wex: WalletExecutionContext, + req: AcceptBankIntegratedWithdrawalRequest, +): Promise<AcceptWithdrawalResponse> { + return await acceptBankIntegratedWithdrawal(wex, { + selectedExchange: req.exchangeBaseUrl, + talerWithdrawUri: req.talerWithdrawUri, + forcedDenomSel: req.forcedDenomSel, + restrictAge: req.restrictAge, + amount: req.amount, + }); +} + +async function handleGetCurrencySpecification( + wex: WalletExecutionContext, + req: GetCurrencySpecificationRequest, +): Promise<GetCurrencySpecificationResponse> { + const spec = await wex.runWalletDbTx(async (tx) => { + return tx.getCurrencyInfo(req.scope); + }); + if (spec) { + if ( + wex.ws.devExperimentState.fakeDemoShortcuts != null && + req.scope.type === ScopeType.Exchange && + req.scope.url === "https://exchange.demo.taler.net/" + ) { + spec.currencySpec.common_amounts = + wex.ws.devExperimentState.fakeDemoShortcuts; + } + return { + currencySpecification: spec.currencySpec, + }; + } + // Hard-coded mock for KUDOS and TESTKUDOS + if (req.scope.currency === "KUDOS") { + const kudosResp: GetCurrencySpecificationResponse = { + currencySpecification: { + name: "Kudos (Taler Demonstrator)", + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": "ク", + }, + }, + }; + return kudosResp; + } else if (req.scope.currency === "TESTKUDOS") { + const testkudosResp: GetCurrencySpecificationResponse = { + currencySpecification: { + name: "Test (Taler Unstable Demonstrator)", + num_fractional_input_digits: 0, + num_fractional_normal_digits: 0, + num_fractional_trailing_zero_digits: 0, + alt_unit_names: { + "0": "テ", + }, + }, + }; + return testkudosResp; + } + const defaultResp: GetCurrencySpecificationResponse = { + currencySpecification: { + name: req.scope.currency, + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": req.scope.currency, + }, + }, + }; + return defaultResp; +} + +export async function handleHintApplicationResumed( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<EmptyObject> { + logger.info("handling hintApplicationResumed"); + await restartAllRunningTasks(wex); + return {}; +} + +export async function handleTestingRunFixup( + wex: WalletExecutionContext, + req: RunFixupRequest, +): Promise<EmptyObject> { + for (const fixup of walletDbFixups) { + if (fixup.name != req.id) { + continue; + } + await wex.runLegacyWalletDbTx(async (tx) => { + await fixup.fn(tx); + await rematerializeTransactions(wex, tx.wtx); + }); + return {}; + } + throw Error("fixup not found"); +} + +async function handleGetVersion( + wex: WalletExecutionContext, +): Promise<WalletCoreVersion> { + const result: WalletCoreVersion = { + implementationSemver: walletCoreBuildInfo.implementationSemver, + implementationGitHash: walletCoreBuildInfo.implementationGitHash, + hash: undefined, + version: WALLET_CORE_API_PROTOCOL_VERSION, + exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, + merchant: WALLET_MERCHANT_PROTOCOL_VERSION, + bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION, + bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION, + corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION, + bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION, + devMode: wex.ws.config.testing.devModeActive, + }; + return result; +} + +export async function handleConvertIbanAccountFieldToPayto( + wex: WalletExecutionContext, + req: ConvertIbanAccountFieldToPaytoRequest, +): Promise<ConvertIbanAccountFieldToPaytoResponse> { + const strippedInput = req.value.replace(/[- ]/g, ""); + if (req.currency === "HUF") { + const iban = convertHUF_BBANtoIBAN(strippedInput); + if (Result.isOk(iban)) { + return { + ok: true, + paytoUri: `payto://iban/${iban.value}`, + type: "iban", + }; + } else { + return { + ok: false, + }; + } + } + if (wex.ws.devExperimentState.fakeChfBban && req.currency === "CHF") { + const iban = convertCHF_BBANtoIBAN(strippedInput); + if (Result.isOk(iban)) { + return { + ok: true, + paytoUri: `payto://iban/${iban.value}`, + type: "iban", + }; + } else { + return { + ok: false, + }; + } + } + const parsedIban = parseIban(strippedInput); + if (Result.isOk(parsedIban)) { + return { + ok: true, + paytoUri: `payto://iban/${strippedInput}`, + type: "iban", + }; + } else { + return { + ok: false, + }; + } +} + +export async function handleConvertIbanPaytoToAccountField( + wex: WalletExecutionContext, + req: ConvertIbanPaytoToAccountFieldRequest, +): Promise<ConvertIbanPaytoToAccountFieldResponse> { + const payto = Result.unpack(Paytos.fromString(req.paytoUri)); + const iban = payto.normalizedPath; + if (iban.startsWith("HU")) { + let bban = iban.slice(4); + if (bban.endsWith("00000000")) { + bban = bban.slice(0, bban.length - 8); + } + return { + type: "bban", + value: bban, + }; + } else if (wex.ws.devExperimentState.fakeChfBban && iban.startsWith("CH")) { + return { + type: "bban", + value: iban.slice(4), + }; + } + return { + type: "iban", + value: iban, + }; +} + +export async function handleGetFlightRecords( + wex: WalletExecutionContext, + _req: EmptyObject, +): Promise<TestingGetFlightRecordsResponse> { + return { flightRecords: wex.ws.flightRecords }; +} + +export async function handleGetDefaultExchanges( + wex: WalletExecutionContext, + _req: EmptyObject, +): Promise<GetDefaultExchangesResponse> { + const defaultExchanges: GetDefaultExchangesResponse["defaultExchanges"] = []; + const myExchanges = await listExchanges(wex, { + filterByType: "prod", + }); + for (const exch of myExchanges.exchanges) { + switch (exch.exchangeEntryStatus) { + case ExchangeEntryStatus.Ephemeral: + continue; + } + if (exch.currency === "UNKNOWN") { + continue; + } + defaultExchanges.push({ + currency: exch.currency, + currencySpec: exch.currencySpec, + talerUri: TalerUris.stringify({ + type: TalerUriAction.WithdrawExchange, + exchangeBaseUrl: exch.exchangeBaseUrl as HostPortPath, + }), + }); + } + if (wex.ws.devExperimentState.fakeDefaultExchangeDemo) { + defaultExchanges.push({ + talerUri: "taler://withdraw-exchange/exchange.demo.taler.net/", + currency: "KUDOS", + currencySpec: { + name: "Kudos", + common_amounts: ["KUDOS:5", "KUDOS:10", "KUDOS:25", "KUDOS:50"], + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": "ク", + }, + }, + }); + } + return { + defaultExchanges, + }; +} + +export async function handleTestingCorruptWithdrawalCoinSel( + wex: WalletExecutionContext, + req: TestingCorruptWithdrawalCoinSelRequest, +): Promise<EmptyObject> { + const txId = parseTransactionIdentifier(req.transactionId); + if (txId?.tag !== TransactionType.Withdrawal) { + throw Error("expected withdrawal transaction ID"); + } + await wex.runWalletDbTx(async (tx) => { + const wg = await tx.getWithdrawalGroup(txId.withdrawalGroupId); + if (!wg) { + return; + } + if (wg.denomsSel && (wg.denomsSel.selectedDenoms.length ?? 0) > 0) { + wg.denomsSel.selectedDenoms[0].denomPubHash = encodeCrock( + getRandomBytes(64), + ); + await tx.upsertWithdrawalGroup(wg); + } + }); + return {}; +} + +export async function handleGetDiagnostics( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise<TestingGetDiagnosticsResponse> { + const cnt: Record<string, number> = {}; + const exchangeEntries: TestingGetDiagnosticsResponse["exchangeEntries"] = []; + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + cnt["coinAvailability"] = await tx.coinAvailability.count(); + cnt["coins"] = await tx.coins.count(); + cnt["denominationFamilies"] = await tx.denominationFamilies.count(); + cnt["denominations"] = await tx.denominations.count(); + cnt["exchangeDetails"] = await tx.exchangeDetails.count(); + cnt["exchangeSignKeys"] = await tx.exchangeSignKeys.count(); + cnt["exchanges"] = await tx.exchanges.count(); + for (const exch of await tx.exchanges.getAll()) { + const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll( + exch.baseUrl, + ); + let numWithdrawableDenoms = 0; + let numCandidateWithdrawableDenoms = 0; + for (let i = 0; i < denoms.length; i++) { + const d = denoms[i]; + if (isWithdrawableDenom(d)) { + numWithdrawableDenoms++; + } + if (isCandidateWithdrawableDenomRec(d)) { + numCandidateWithdrawableDenoms++; + } + } + exchangeEntries.push({ + exchangeBaseUrl: exch.baseUrl, + numDenoms: denoms.length, + numCandidateWithdrawableDenoms, + numWithdrawableDenoms, + }); + } + }); + return { + version: 0, + idbObjectStoreCounts: cnt, + exchangeEntries, + }; +} + +export async function handleTestingWaitExchangeReady( + wex: WalletExecutionContext, + req: TestingWaitExchangeReadyRequest, +): Promise<EmptyObject> { + await waitReadyExchange(wex, req.exchangeBaseUrl, { + noBail: req.noBail, + forceUpdate: req.forceUpdate, + waitAutoRefresh: req.waitAutoRefresh, + }); + return {}; +} + +export async function handleTestingWaitBalance( + wex: WalletExecutionContext, + req: TestingWaitBalanceRequest, +): Promise<EmptyObject> { + await testingWaitBalance(wex, req); + return {}; +} + +export async function handleGetPerformanceStats( + wex: WalletExecutionContext, + req: GetPerformanceStatsRequest, +): Promise<GetPerformanceStatsResponse> { + return { + stats: PerformanceTable.limit(wex.ws.performanceStats, req.limit), + }; +} + +interface HandlerWithValidator<Tag extends WalletApiOperation> { + codec: Codec<WalletCoreRequestType<Tag>>; + handler: ( + wex: WalletExecutionContext, + req: WalletCoreRequestType<Tag>, + ) => Promise<WalletCoreResponseType<Tag>>; +} + +const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { + [WalletApiOperation.CancelProgressToken]: { + codec: codecForCancelProgressToken(), + handler: handleCancelProgressToken, + }, + [WalletApiOperation.RetryProgressTokenNow]: { + codec: codecForRetryProgressTokenNowRequest(), + handler: handleRetryProgressTokenNow, + }, + [WalletApiOperation.TestingWaitExchangeReady]: { + codec: codecForTestingWaitExchangeReadyRequest(), + handler: handleTestingWaitExchangeReady, + }, + [WalletApiOperation.TestingWaitBalance]: { + codec: codecForTestingWaitBalanceRequest(), + handler: handleTestingWaitBalance, + }, + [WalletApiOperation.TestingCorruptWithdrawalCoinSel]: { + codec: codecForTestingCorruptWithdrawalCoinSelRequest(), + handler: handleTestingCorruptWithdrawalCoinSel, + }, + [WalletApiOperation.GetDefaultExchanges]: { + codec: codecForEmptyObject(), + handler: handleGetDefaultExchanges, + }, + [WalletApiOperation.TestingGetFlightRecords]: { + codec: codecForEmptyObject(), + handler: handleGetFlightRecords, + }, + [WalletApiOperation.GetDiagnostics]: { + codec: codecForEmptyObject(), + handler: handleGetDiagnostics, + }, + [WalletApiOperation.ConvertIbanAccountFieldToPayto]: { + codec: codecForConvertIbanAccountFieldToPaytoRequest(), + handler: handleConvertIbanAccountFieldToPayto, + }, + [WalletApiOperation.ConvertIbanPaytoToAccountField]: { + codec: codecForConvertIbanPaytoToAccountFieldRequest(), + handler: handleConvertIbanPaytoToAccountField, + }, + [WalletApiOperation.TestingGetPerformanceStats]: { + codec: codecForGetPerformanceStatsRequest(), + handler: handleGetPerformanceStats, + }, + [WalletApiOperation.TestingWaitExchangeState]: { + codec: codecForAny(), + handler: handleTestingWaitExchangeState, + }, + [WalletApiOperation.GetDonauStatements]: { + codec: codecForGetDonauStatementsRequest(), + handler: handleGetDonauStatements, + }, + [WalletApiOperation.GetDonau]: { + codec: codecForEmptyObject(), + handler: handleGetDonau, + }, + [WalletApiOperation.SetDonau]: { + codec: codecForSetDonauRequest(), + handler: handleSetDonau, + }, + [WalletApiOperation.AddContact]: { + codec: codecForAddContactRequest(), + handler: addContact, + }, + [WalletApiOperation.DeleteContact]: { + codec: codecForDeleteContactRequest(), + handler: deleteContact, + }, + [WalletApiOperation.GetContacts]: { + codec: codecForEmptyObject(), + handler: listContacts, + }, + [WalletApiOperation.RegisterAlias]: { + codec: codecForTaldirRegistrationRequest(), + handler: registerAlias, + }, + [WalletApiOperation.CompleteRegisterAlias]: { + codec: codecForTaldirRegistrationCompletionRequest(), + handler: completeAliasRegistration, + }, + [WalletApiOperation.LookupAlias]: { + codec: codecForTaldirLookupRequest(), + handler: lookupAlias, + }, + [WalletApiOperation.InitializeMailbox]: { + codec: codecForMailboxBaseUrl(), + handler: createNewMailbox, + }, + [WalletApiOperation.GetMailbox]: { + codec: codecForMailboxBaseUrl(), + handler: getMailbox, + }, + [WalletApiOperation.SendTalerUriMailboxMessage]: { + codec: codecForSendTalerUriMailboxMessageRequest(), + handler: sendTalerUriMessage, + }, + [WalletApiOperation.AddMailboxMessage]: { + codec: codecForAddMailboxMessageRequest(), + handler: addMailboxMessage, + }, + [WalletApiOperation.DeleteMailboxMessage]: { + codec: codecForDeleteMailboxMessageRequest(), + handler: deleteMailboxMessage, + }, + [WalletApiOperation.GetMailboxMessages]: { + codec: codecForEmptyObject(), + handler: listMailboxMessages, + }, + [WalletApiOperation.RefreshMailbox]: { + codec: codecForMailboxConfiguration(), + handler: refreshMailbox, + }, + [WalletApiOperation.TestingGetDbStats]: { + codec: codecForEmptyObject(), + handler: async (wex) => { + return wex.ws.dbImplementation.getStats(); + }, + }, + [WalletApiOperation.ExportDbToFile]: { + codec: codecForExportDbToFileRequest(), + handler: handleExportDbToFile, + }, + [WalletApiOperation.HintApplicationResumed]: { + codec: codecForEmptyObject(), + handler: handleHintApplicationResumed, + }, + [WalletApiOperation.TestingRunFixup]: { + codec: codecForRunFixupRequest(), + handler: handleTestingRunFixup, + }, + [WalletApiOperation.AbortTransaction]: { + codec: codecForAbortTransaction(), + handler: handleAbortTransaction, + }, + [WalletApiOperation.CreateStoredBackup]: { + codec: codecForEmptyObject(), + handler: handleCreateStoredBackup, + }, + [WalletApiOperation.DeleteStoredBackup]: { + codec: codecForDeleteStoredBackupRequest(), + handler: handleDeleteStoredBackup, + }, + [WalletApiOperation.ListStoredBackups]: { + codec: codecForEmptyObject(), + handler: listStoredBackups, + }, + [WalletApiOperation.CompleteExchangeBaseUrl]: { + codec: codecForCompleteBaseUrlRequest(), + handler: handleCompleteExchangeBaseUrl, + }, + [WalletApiOperation.SetWalletRunConfig]: { + codec: codecForInitRequest(), + handler: handleSetWalletRunConfig, + }, + // Alias for SetWalletRunConfig + [WalletApiOperation.InitWallet]: { + codec: codecForInitRequest(), + handler: handleSetWalletRunConfig, + }, + [WalletApiOperation.RecoverStoredBackup]: { + codec: codecForRecoverStoredBackupRequest(), + handler: handleRecoverStoredBackup, + }, + [WalletApiOperation.WithdrawTestkudos]: { + codec: codecForEmptyObject(), + handler: handleWithdrawTestkudos, + }, + [WalletApiOperation.WithdrawTestBalance]: { + codec: codecForWithdrawTestBalance(), + handler: handleWithdrawTestBalance, + }, + [WalletApiOperation.RunIntegrationTest]: { + codec: codecForIntegrationTestArgs(), + handler: handleRunIntegrationTest, + }, + [WalletApiOperation.RunIntegrationTestV2]: { + codec: codecForIntegrationTestV2Args(), + handler: handleRunIntegrationTestV2, + }, + [WalletApiOperation.ValidateIban]: { + codec: codecForValidateIbanRequest(), + handler: handleValidateIban, + }, + [WalletApiOperation.TestPay]: { + codec: codecForTestPayArgs(), + handler: testPay, + }, + [WalletApiOperation.GetTransactions]: { + codec: codecForTransactionsRequest(), + handler: getTransactions, + }, + [WalletApiOperation.GetTransactionsV2]: { + codec: codecForGetTransactionsV2Request(), + handler: getTransactionsV2, + }, + [WalletApiOperation.GetTransactionById]: { + codec: codecForTransactionByIdRequest(), + handler: getTransactionById, + }, + [WalletApiOperation.AddExchange]: { + codec: codecForAddExchangeRequest(), + handler: handleAddExchange, + }, + [WalletApiOperation.TestingPing]: { + codec: codecForEmptyObject(), + handler: async () => ({}), + }, + [WalletApiOperation.UpdateExchangeEntry]: { + codec: codecForUpdateExchangeEntryRequest(), + handler: handleUpdateExchangeEntry, + }, + [WalletApiOperation.TestingGetDenomStats]: { + codec: codecForTestingGetDenomStatsRequest(), + handler: handleTestingGetDenomStats, + }, + [WalletApiOperation.ListExchanges]: { + codec: codecForListExchangesRequest(), + handler: listExchanges, + }, + [WalletApiOperation.GetExchangeEntryByUrl]: { + codec: codecForGetExchangeEntryByUrlRequest(), + handler: lookupExchangeByUri, + }, + [WalletApiOperation.GetExchangeDetailedInfo]: { + codec: codecForGetExchangeEntryByUrlRequest(), + handler: (wex, req) => getExchangeDetailedInfo(wex, req.exchangeBaseUrl), + }, + [WalletApiOperation.ListBankAccounts]: { + codec: codecForListBankAccounts(), + handler: handleListBankAccounts, + }, + [WalletApiOperation.GetBankAccountById]: { + codec: codecForGetBankAccountByIdRequest(), + handler: handleGetBankAccountById, + }, + [WalletApiOperation.AddBankAccount]: { + codec: codecForAddBankAccountRequest(), + handler: handleAddBankAccount, + }, + [WalletApiOperation.ForgetBankAccount]: { + codec: codecForForgetBankAccount(), + handler: handleForgetBankAccount, + }, + [WalletApiOperation.GetWithdrawalDetailsForUri]: { + codec: codecForGetWithdrawalDetailsForUri(), + handler: (wex, req) => + getWithdrawalDetailsForUri(wex, req.talerWithdrawUri), + }, + [WalletApiOperation.TestingGetReserveHistory]: { + codec: codecForTestingGetReserveHistoryRequest(), + handler: handleTestingGetReserveHistory, + }, + [WalletApiOperation.AcceptManualWithdrawal]: { + codec: codecForAcceptManualWithdrawalRequest(), + handler: handleAcceptManualWithdrawal, + }, + [WalletApiOperation.GetWithdrawalDetailsForAmount]: { + codec: codecForGetWithdrawalDetailsForAmountRequest(), + handler: getWithdrawalDetailsForAmount, + }, + [WalletApiOperation.GetBalances]: { + codec: codecForEmptyObject(), + handler: getBalances, + }, + [WalletApiOperation.GetBalanceDetail]: { + codec: codecForGetBalanceDetailRequest(), + handler: getBalanceDetail, + }, + [WalletApiOperation.SetExchangeTosAccepted]: { + codec: codecForAcceptExchangeTosRequest(), + handler: async (wex, req) => { + await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl); + return {}; + }, + }, + [WalletApiOperation.SetExchangeTosForgotten]: { + codec: codecForAcceptExchangeTosRequest(), + handler: async (wex, req) => { + await forgetExchangeTermsOfService(wex, req.exchangeBaseUrl); + return {}; + }, + }, + [WalletApiOperation.AcceptBankIntegratedWithdrawal]: { + codec: codecForAcceptBankIntegratedWithdrawalRequest(), + handler: handleAcceptBankIntegratedWithdrawal, + }, + [WalletApiOperation.ConfirmWithdrawal]: { + codec: codecForConfirmWithdrawalRequestRequest(), + handler: confirmWithdrawal, + }, + [WalletApiOperation.PrepareBankIntegratedWithdrawal]: { + codec: codecForPrepareBankIntegratedWithdrawalRequest(), + handler: prepareBankIntegratedWithdrawal, + }, + [WalletApiOperation.GetExchangeTos]: { + codec: codecForGetExchangeTosRequest(), + handler: handleGetExchangeTos, + }, + [WalletApiOperation.SharePayment]: { + codec: codecForSharePaymentRequest(), + handler: handleSharePayment, + }, + [WalletApiOperation.PrepareWithdrawExchange]: { + codec: codecForPrepareWithdrawExchangeRequest(), + handler: handlePrepareWithdrawExchange, + }, + [WalletApiOperation.CheckPayForTemplate]: { + codec: codecForCheckPayTemplateRequest(), + handler: checkPayForTemplate, + }, + [WalletApiOperation.PreparePayForUri]: { + codec: codecForPreparePayRequest(), + handler: (wex, req) => preparePayForUri(wex, req.talerPayUri), + }, + [WalletApiOperation.PreparePayForTemplate]: { + codec: codecForPreparePayTemplateRequest(), + handler: preparePayForTemplate, + }, + [WalletApiOperation.PreparePayForUriV2]: { + codec: codecForPreparePayRequest(), + handler: (wex, req) => preparePayForUriV2(wex, req.talerPayUri), + }, + [WalletApiOperation.PreparePayForTemplateV2]: { + codec: codecForPreparePayTemplateRequest(), + handler: preparePayForTemplateV2, + }, + [WalletApiOperation.GetQrCodesForPayto]: { + codec: codecForGetQrCodesForPaytoRequest(), + handler: handleGetQrCodesForPayto, + }, + [WalletApiOperation.GetChoicesForPayment]: { + codec: codecForGetChoicesForPaymentRequest(), + handler: handleGetChoicesForPayment, + }, + [WalletApiOperation.ConfirmPay]: { + codec: codecForConfirmPayRequest(), + handler: handleConfirmPay, + }, + [WalletApiOperation.ListDiscounts]: { + codec: codecForListDiscountsRequest(), + handler: handleListDiscounts, + }, + [WalletApiOperation.DeleteDiscount]: { + codec: codecForDeleteDiscountRequest(), + handler: handleDeleteDiscount, + }, + [WalletApiOperation.ListSubscriptions]: { + codec: codecForListSubscriptionsRequest(), + handler: handleListSubscriptions, + }, + [WalletApiOperation.DeleteSubscription]: { + codec: codecForDeleteSubscriptionRequest(), + handler: handleDeleteSubscription, + }, + [WalletApiOperation.SuspendTransaction]: { + codec: codecForSuspendTransaction(), + handler: handleSuspendTransaction, + }, + [WalletApiOperation.GetActiveTasks]: { + codec: codecForEmptyObject(), + handler: handleGetActiveTasks, + }, + [WalletApiOperation.FailTransaction]: { + codec: codecForFailTransactionRequest(), + handler: handleFailTransaction, + }, + [WalletApiOperation.ResumeTransaction]: { + codec: codecForResumeTransaction(), + handler: async (wex, req) => { + await resumeTransaction(wex, req.transactionId); + return {}; + }, + }, + [WalletApiOperation.DumpCoins]: { + codec: codecForEmptyObject(), + handler: dumpCoins, + }, + [WalletApiOperation.SetCoinSuspended]: { + codec: codecForSetCoinSuspendedRequest(), + handler: async (wex, req) => { + await setCoinSuspended(wex, req.coinPub, req.suspended); + return {}; + }, + }, + [WalletApiOperation.TestingGetSampleTransactions]: { + codec: codecForEmptyObject(), + handler: handleTestingGetSampleTransactions, + }, + [WalletApiOperation.StartRefundQueryForUri]: { + codec: codecForPrepareRefundRequest(), + handler: (wex, req) => startRefundQueryForUri(wex, req.talerRefundUri), + }, + [WalletApiOperation.StartRefundQuery]: { + codec: codecForStartRefundQueryRequest(), + handler: handleStartRefundQuery, + }, + [WalletApiOperation.TestingWaitTransactionState]: { + codec: codecForAny(), + handler: async (wex, req) => { + await waitTransactionState(wex, req); + return {}; + }, + }, + [WalletApiOperation.GetCurrencySpecification]: { + codec: codecForGetCurrencyInfoRequest(), + handler: handleGetCurrencySpecification, + }, + [WalletApiOperation.HintNetworkAvailability]: { + codec: codecForHintNetworkAvailabilityRequest(), + handler: handleHintNetworkAvailability, + }, + [WalletApiOperation.ConvertDepositAmount]: { + codec: codecForConvertAmountRequest, + handler: convertDepositAmount, + }, + [WalletApiOperation.GetMaxDepositAmount]: { + codec: codecForGetMaxDepositAmountRequest(), + handler: getMaxDepositAmount, + }, + [WalletApiOperation.GetMaxPeerPushDebitAmount]: { + codec: codecForGetMaxPeerPushDebitAmountRequest(), + handler: getMaxPeerPushDebitAmount, + }, + [WalletApiOperation.CheckDeposit]: { + codec: codecForCheckDepositRequest(), + handler: checkDepositGroup, + }, + [WalletApiOperation.CreateDepositGroup]: { + codec: codecForCreateDepositGroupRequest(), + handler: createDepositGroup, + }, + [WalletApiOperation.DeleteTransaction]: { + codec: codecForDeleteTransactionRequest(), + handler: async (wex, req) => { + await deleteTransaction(wex, req.transactionId); + return {}; + }, + }, + [WalletApiOperation.RetryTransaction]: { + codec: codecForRetryTransactionRequest(), + handler: async (wex, req) => { + await retryTransaction(wex, req.transactionId); + return {}; + }, + }, + [WalletApiOperation.TestCrypto]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + return await wex.cryptoApi.hashString({ str: "hello world" }); + }, + }, + [WalletApiOperation.ClearDb]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + await clearDatabase(wex.db.idbHandle()); + wex.ws.flightRecords = []; + wex.ws.clearAllCaches(); + await wex.taskScheduler.reload(); + return {}; + }, + }, + [WalletApiOperation.Recycle]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + throw Error("not implemented"); + }, + }, + [WalletApiOperation.ExportDb]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + const dbDump = await exportDb(wex.ws.idbFactory); + return dbDump; + }, + }, + [WalletApiOperation.GetDepositWireTypes]: { + codec: codecForGetDepositWireTypesRequest(), + handler: handleGetDepositWireTypes, + }, + [WalletApiOperation.GetDepositWireTypesForCurrency]: { + codec: codecForGetDepositWireTypesForCurrencyRequest(), + handler: handleGetDepositWireTypesForCurrency, + }, + [WalletApiOperation.ListGlobalCurrencyExchanges]: { + codec: codecForEmptyObject(), + handler: handleListGlobalCurrencyExchanges, + }, + [WalletApiOperation.ListGlobalCurrencyAuditors]: { + codec: codecForEmptyObject(), + handler: handleListGlobalCurrencyAuditors, + }, + [WalletApiOperation.AddGlobalCurrencyExchange]: { + codec: codecForAddGlobalCurrencyExchangeRequest(), + handler: handleAddGlobalCurrencyExchange, + }, + [WalletApiOperation.RemoveGlobalCurrencyExchange]: { + codec: codecForRemoveGlobalCurrencyExchangeRequest(), + handler: handleRemoveGlobalCurrencyExchange, + }, + [WalletApiOperation.AddGlobalCurrencyAuditor]: { + codec: codecForAddGlobalCurrencyAuditorRequest(), + handler: handleAddGlobalCurrencyAuditor, + }, + [WalletApiOperation.TestingWaitTasksDone]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + await waitTasksDone(wex); + return {}; + }, + }, + [WalletApiOperation.TestingResetAllRetries]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + await retryAll(wex); + return {}; + }, + }, + [WalletApiOperation.RemoveGlobalCurrencyAuditor]: { + codec: codecForRemoveGlobalCurrencyAuditorRequest(), + handler: handleRemoveGlobalCurrencyAuditor, + }, + [WalletApiOperation.ImportDb]: { + codec: codecForImportDbRequest(), + handler: handleImportDb, + }, + [WalletApiOperation.ImportDbFromFile]: { + codec: codecForImportDbFromFileRequest(), + handler: handleImportDbFromFile, + }, + [WalletApiOperation.CheckPeerPushDebit]: { + codec: codecForCheckPeerPushDebitRequest(), + handler: checkPeerPushDebit, + }, + [WalletApiOperation.CheckPeerPushDebitV2]: { + codec: codecForCheckPeerPushDebitRequest(), + handler: checkPeerPushDebitV2, + }, + [WalletApiOperation.InitiatePeerPushDebit]: { + codec: codecForInitiatePeerPushDebitRequest(), + handler: initiatePeerPushDebit, + }, + [WalletApiOperation.PreparePeerPushCredit]: { + codec: codecForPreparePeerPushCreditRequest(), + handler: preparePeerPushCredit, + }, + [WalletApiOperation.ConfirmPeerPushCredit]: { + codec: codecForConfirmPeerPushPaymentRequest(), + handler: confirmPeerPushCredit, + }, + [WalletApiOperation.CheckPeerPullCredit]: { + codec: codecForPreparePeerPullPaymentRequest(), + handler: checkPeerPullCredit, + }, + [WalletApiOperation.InitiatePeerPullCredit]: { + codec: codecForInitiatePeerPullPaymentRequest(), + handler: initiatePeerPullPayment, + }, + [WalletApiOperation.PreparePeerPullDebit]: { + codec: codecForCheckPeerPullPaymentRequest(), + handler: preparePeerPullDebit, + }, + [WalletApiOperation.ConfirmPeerPullDebit]: { + codec: codecForAcceptPeerPullPaymentRequest(), + handler: confirmPeerPullDebit, + }, + [WalletApiOperation.ApplyDevExperiment]: { + codec: codecForApplyDevExperiment(), + handler: async (wex, req) => { + await applyDevExperiment(wex, req.devExperimentUri); + return {}; + }, + }, + [WalletApiOperation.Shutdown]: { + codec: codecForEmptyObject(), + handler: handleShutdown, + }, + [WalletApiOperation.GetVersion]: { + codec: codecForEmptyObject(), + handler: handleGetVersion, + }, + [WalletApiOperation.TestingWaitTransactionsFinal]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + await waitUntilAllTransactionsFinal(wex); + return {}; + }, + }, + [WalletApiOperation.TestingWaitRefreshesFinal]: { + codec: codecForEmptyObject(), + handler: async (wex, req) => { + await waitUntilRefreshesDone(wex); + return {}; + }, + }, + [WalletApiOperation.TestingSetTimetravel]: { + codec: codecForTestingSetTimetravelRequest(), + handler: async (wex, req) => { + await handleTestingSetTimetravel(wex, req); + return {}; + }, + }, + [WalletApiOperation.DeleteExchange]: { + codec: codecForDeleteExchangeRequest(), + handler: handleDeleteExchange, + }, + [WalletApiOperation.GetExchangeResources]: { + codec: codecForGetExchangeResourcesRequest(), + handler: async (wex, req) => { + return await getExchangeResources(wex, req.exchangeBaseUrl); + }, + }, + [WalletApiOperation.CanonicalizeBaseUrl]: { + codec: codecForCanonicalizeBaseUrlRequest(), + handler: handleCanonicalizeBaseUrl, + }, + [WalletApiOperation.ForceRefresh]: { + codec: codecForForceRefreshRequest(), + handler: async (wex, req) => { + await forceRefresh(wex, req); + return {}; + }, + }, + [WalletApiOperation.ListAssociatedRefreshes]: { + codec: codecForAny(), + handler: async (wex, req) => { + throw Error("not implemented"); + }, + }, + [WalletApiOperation.GetBankingChoicesForPayto]: { + codec: codecForGetBankingChoicesForPaytoRequest(), + handler: handleGetBankingChoicesForPayto, + }, + [WalletApiOperation.StartExchangeWalletKyc]: { + codec: codecForStartExchangeWalletKycRequest(), + handler: handleStartExchangeWalletKyc, + }, + [WalletApiOperation.TestingWaitExchangeWalletKyc]: { + codec: codecForTestingWaitWalletKycRequest(), + handler: handleTestingWaitExchangeWalletKyc, + }, + [WalletApiOperation.TestingPlanMigrateExchangeBaseUrl]: { + codec: codecForTestingPlanMigrateExchangeBaseUrlRequest(), + handler: handleTestingPlanMigrateExchangeBaseUrl, + }, +}; + +/** + * Implementation of the "wallet-core" API. + */ +export async function dispatchRequestInternal( + wex: WalletExecutionContext, + operation: WalletApiOperation, + payload: unknown, +): Promise<WalletCoreResponseType<typeof operation>> { + if (!wex.ws.initCalled && operation !== WalletApiOperation.InitWallet) { + throw Error( + `wallet must be initialized before running operation ${operation}`, + ); + } + + const h: HandlerWithValidator<any> = handlers[operation]; + + if (!h) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, + { + operation, + }, + "unknown operation", + ); + } + + const req = h.codec.decode(payload); + return await h.handler(wex, req); +} diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -29,89 +29,15 @@ import { IDBDatabase, } from "@gnu-taler/idb-bridge"; import { - AbortTransactionRequest, - AbsoluteTime, - AcceptBankIntegratedWithdrawalRequest, - AcceptManualWithdrawalRequest, - AcceptManualWithdrawalResult, - AcceptWithdrawalResponse, - ActiveTask, - AddBankAccountRequest, - AddBankAccountResponse, - AddExchangeRequest, - AddExchangeResponse, - AddGlobalCurrencyAuditorRequest, - AddGlobalCurrencyExchangeRequest, AmountJson, - AmountString, - Amounts, AsyncCondition, CancellationToken, - CanonicalizeBaseUrlRequest, - CanonicalizeBaseUrlResponse, - Codec, - CoinDumpJson, - CoinStatus, - CompleteBaseUrlRequest, - CompleteBaseUrlResult, - ConfirmPayRequest, - ConfirmPayResult, - ConvertIbanAccountFieldToPaytoRequest, - ConvertIbanAccountFieldToPaytoResponse, - ConvertIbanPaytoToAccountFieldRequest, - ConvertIbanPaytoToAccountFieldResponse, CoreApiResponse, - CreateStoredBackupResponse, - DeleteDiscountRequest, - DeleteExchangeRequest, - DeleteStoredBackupRequest, - DeleteSubscriptionRequest, DenominationInfo, + Cache, Duration, - EmptyObject, - ExchangeEntryStatus, - ExportDbToFileRequest, - ExportDbToFileResponse, - FailTransactionRequest, FlightRecordEntry, FlightRecordEvent, - ForgetBankAccountRequest, - GetActiveTasksResponse, - GetBankAccountByIdRequest, - GetBankAccountByIdResponse, - GetBankingChoicesForPaytoRequest, - GetBankingChoicesForPaytoResponse, - GetChoicesForPaymentRequest, - GetChoicesForPaymentResult, - GetCurrencySpecificationRequest, - GetCurrencySpecificationResponse, - GetDefaultExchangesResponse, - GetDepositWireTypesForCurrencyRequest, - GetDepositWireTypesForCurrencyResponse, - GetDepositWireTypesRequest, - GetDepositWireTypesResponse, - GetExchangeTosRequest, - GetExchangeTosResult, - GetPerformanceStatsRequest, - GetPerformanceStatsResponse, - GetQrCodesForPaytoRequest, - GetQrCodesForPaytoResponse, - HintNetworkAvailabilityRequest, - HostPortPath, - ImportDbFromFileRequest, - ImportDbRequest, - InitRequest, - InitResponse, - IntegrationTestArgs, - IntegrationTestV2Args, - ListBankAccountsRequest, - ListBankAccountsResponse, - ListDiscountsRequest, - ListDiscountsResponse, - ListGlobalCurrencyAuditorsResponse, - ListGlobalCurrencyExchangesResponse, - ListSubscriptionsRequest, - ListSubscriptionsResponse, Logger, LongpollQueue, NotificationType, @@ -120,307 +46,54 @@ import { ObservableHttpClientLibrary, OpenedPromise, PartialWalletRunConfig, - PaytoType, - Paytos, PerformanceTable, - PrepareWithdrawExchangeRequest, - PrepareWithdrawExchangeResponse, - RecoverStoredBackupRequest, - RemoveGlobalCurrencyAuditorRequest, - RemoveGlobalCurrencyExchangeRequest, Result, - RunFixupRequest, - ScopeType, - SharePaymentRequest, - SharePaymentResult, - StartRefundQueryRequest, - StoredBackupList, - SuspendTransactionRequest, - TalerBankIntegrationHttpClient, TalerError, TalerErrorCode, TalerExchangeHttpClient, TalerMerchantInstanceHttpClient, TalerPreciseTimestamp, - TalerProtocolTimestamp, - TalerUriAction, - TalerUris, - TestingCorruptWithdrawalCoinSelRequest, - TestingGetDenomStatsRequest, - TestingGetDenomStatsResponse, - TestingGetDiagnosticsResponse, - TestingGetFlightRecordsResponse, - TestingGetReserveHistoryRequest, - TestingSetTimetravelRequest, - TestingWaitBalanceRequest, - TestingWaitExchangeReadyRequest, TimerAPI, TimerGroup, - TransactionType, - TransactionsResponse, - UpdateExchangeEntryRequest, - ValidateIbanRequest, - ValidateIbanResponse, - WalletBankAccountInfo, - WalletCoreVersion, WalletNotification, WalletRunConfig, - WireTypeDetails, - WithdrawTestBalanceRequest, assertUnreachable, - canonicalizeBaseUrl, - checkDbInvariant, - codecForAbortTransaction, - codecForAcceptBankIntegratedWithdrawalRequest, - codecForAcceptExchangeTosRequest, - codecForAcceptManualWithdrawalRequest, - codecForAcceptPeerPullPaymentRequest, - codecForAddBankAccountRequest, - codecForAddContactRequest, - codecForAddExchangeRequest, - codecForAddGlobalCurrencyAuditorRequest, - codecForAddGlobalCurrencyExchangeRequest, - codecForAddMailboxMessageRequest, - codecForAny, - codecForApplyDevExperiment, - codecForCancelProgressToken, - codecForCanonicalizeBaseUrlRequest, - codecForCheckDepositRequest, - codecForCheckPayTemplateRequest, - codecForCheckPeerPullPaymentRequest, - codecForCheckPeerPushDebitRequest, - codecForCompleteBaseUrlRequest, - codecForConfirmPayRequest, - codecForConfirmPeerPushPaymentRequest, - codecForConfirmWithdrawalRequestRequest, - codecForConvertAmountRequest, - codecForConvertIbanAccountFieldToPaytoRequest, - codecForConvertIbanPaytoToAccountFieldRequest, - codecForCreateDepositGroupRequest, - codecForDeleteContactRequest, - codecForDeleteDiscountRequest, - codecForDeleteExchangeRequest, - codecForDeleteMailboxMessageRequest, - codecForDeleteStoredBackupRequest, - codecForDeleteSubscriptionRequest, - codecForDeleteTransactionRequest, - codecForEmptyObject, - codecForExportDbToFileRequest, - codecForFailTransactionRequest, - codecForForceRefreshRequest, - codecForForgetBankAccount, - codecForGetBalanceDetailRequest, - codecForGetBankAccountByIdRequest, - codecForGetBankingChoicesForPaytoRequest, - codecForGetChoicesForPaymentRequest, - codecForGetCurrencyInfoRequest, - codecForGetDepositWireTypesForCurrencyRequest, - codecForGetDepositWireTypesRequest, - codecForGetDonauStatementsRequest, - codecForGetExchangeEntryByUrlRequest, - codecForGetExchangeResourcesRequest, - codecForGetExchangeTosRequest, - codecForGetMaxDepositAmountRequest, - codecForGetMaxPeerPushDebitAmountRequest, - codecForGetPerformanceStatsRequest, - codecForGetQrCodesForPaytoRequest, - codecForGetTransactionsV2Request, - codecForGetWithdrawalDetailsForAmountRequest, - codecForGetWithdrawalDetailsForUri, - codecForHintNetworkAvailabilityRequest, - codecForImportDbFromFileRequest, - codecForImportDbRequest, - codecForInitRequest, - codecForInitiatePeerPullPaymentRequest, - codecForInitiatePeerPushDebitRequest, - codecForIntegrationTestArgs, - codecForIntegrationTestV2Args, - codecForListBankAccounts, - codecForListDiscountsRequest, - codecForListExchangesRequest, - codecForListSubscriptionsRequest, - codecForMailboxBaseUrl, - codecForMailboxConfiguration, - codecForPrepareBankIntegratedWithdrawalRequest, - codecForPreparePayRequest, - codecForPreparePayTemplateRequest, - codecForPreparePeerPullPaymentRequest, - codecForPreparePeerPushCreditRequest, - codecForPrepareRefundRequest, - codecForPrepareWithdrawExchangeRequest, - codecForRecoverStoredBackupRequest, - codecForRemoveGlobalCurrencyAuditorRequest, - codecForRemoveGlobalCurrencyExchangeRequest, - codecForResumeTransaction, - codecForRetryProgressTokenNowRequest, - codecForRetryTransactionRequest, - codecForRunFixupRequest, - codecForSendTalerUriMailboxMessageRequest, - codecForSetCoinSuspendedRequest, - codecForSetDonauRequest, - codecForSharePaymentRequest, - codecForStartExchangeWalletKycRequest, - codecForStartRefundQueryRequest, - codecForSuspendTransaction, - codecForTaldirLookupRequest, - codecForTaldirRegistrationCompletionRequest, - codecForTaldirRegistrationRequest, - codecForTestPayArgs, - codecForTestingCorruptWithdrawalCoinSelRequest, - codecForTestingGetDenomStatsRequest, - codecForTestingGetReserveHistoryRequest, - codecForTestingPlanMigrateExchangeBaseUrlRequest, - codecForTestingSetTimetravelRequest, - codecForTestingWaitBalanceRequest, - codecForTestingWaitExchangeReadyRequest, - codecForTestingWaitWalletKycRequest, - codecForTransactionByIdRequest, - codecForTransactionsRequest, - codecForUpdateExchangeEntryRequest, - codecForValidateIbanRequest, - codecForWithdrawTestBalance, - convertCHF_BBANtoIBAN, - convertHUF_BBANtoIBAN, - encodeCrock, getErrorDetailFromException, - getQrCodesForPayto, - getRandomBytes, j2s, openPromise, - parseIban, performanceDelta, performanceNow, safeStringifyException, - setDangerousTimetravel, - setGlobalLogLevelFromString, - stringifyScopeInfo, - validateIban, } from "@gnu-taler/taler-util"; -import { - readSuccessResponseJsonOrThrow, - type HttpRequestLibrary, -} from "@gnu-taler/taler-util/http"; -import { getBalanceDetail, getBalances } from "./balance.js"; -import { - getMaxDepositAmount, - getMaxPeerPushDebitAmount, -} from "./coinSelection.js"; -import { cancelableFetch } from "./common.js"; -import { addContact, deleteContact, listContacts } from "./contacts.js"; +import { type HttpRequestLibrary } from "@gnu-taler/taler-util/http"; +import { dispatchRequestInternal } from "./requests.js"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { CryptoDispatcher, CryptoWorkerFactory, } from "./crypto/workers/crypto-dispatcher.js"; +import { ConfigRecordKey, WalletDenomination } from "./db-common.js"; import { - ConfigRecordKey, - timestampAbsoluteFromDb, - timestampProtocolToDb, - WalletDenomination, -} from "./db-common.js"; -import { - CoinSourceType, WalletIndexedDbStoresV1, WalletIndexedDbTransaction, applyFixups, - clearDatabase, - exportDb, - importDb, - openStoredBackupsDatabase, openTalerDatabase, - walletDbFixups, } from "./db-indexeddb.js"; import { WalletDbTransaction } from "./dbtx.js"; import { IdbWalletTransaction } from "./dbtx-indexeddb.js"; -import { - isCandidateWithdrawableDenomRec, - isWithdrawableDenom, -} from "./denominations.js"; import { UnverifiedDenomError } from "./denomSelection.js"; -import { checkDepositGroup, createDepositGroup } from "./deposits.js"; -import { - DevExperimentHttpLib, - DevExperimentState, - applyDevExperiment, -} from "./dev-experiments.js"; -import { - handleGetDonau, - handleGetDonauStatements, - handleSetDonau, -} from "./donau.js"; +import { DevExperimentHttpLib, DevExperimentState } from "./dev-experiments.js"; import { OutdatedExchangeError, ReadyExchangeSummary, - acceptExchangeTermsOfService, - deleteExchange, fetchFreshExchange, - forgetExchangeTermsOfService, - getExchangeDetailedInfo, - getExchangeDetailsInTx, - getExchangeResources, - getExchangeTos, - handleStartExchangeWalletKyc, - handleTestingPlanMigrateExchangeBaseUrl, - handleTestingWaitExchangeState, - handleTestingWaitExchangeWalletKyc, - listExchanges, - lookupExchangeByUri, - markExchangeUsed, - resetExchangeRetries, - startUpdateExchangeEntry, - waitReadyExchange, } from "./exchanges.js"; -import { convertDepositAmount } from "./instructedAmountConversion.js"; -import { - addMailboxMessage, - createNewMailbox, - deleteMailboxMessage, - getMailbox, - listMailboxMessages, - refreshMailbox, - sendTalerUriMessage, -} from "./mailbox.js"; import { ObservableDbAccess, ObservableTaskScheduler, observeTalerCrypto, } from "./observable-wrappers.js"; -import { - confirmPay, - getChoicesForPayment, - preparePayForTemplate, - preparePayForTemplateV2, - preparePayForUri, - preparePayForUriV2, - sharePayment, - startQueryRefund, - startRefundQueryForUri, -} from "./pay-merchant.js"; -import { - checkPeerPullCredit, - initiatePeerPullPayment, -} from "./pay-peer-pull-credit.js"; -import { - confirmPeerPullDebit, - preparePeerPullDebit, -} from "./pay-peer-pull-debit.js"; -import { - confirmPeerPushCredit, - preparePeerPushCredit, -} from "./pay-peer-push-credit.js"; -import { - checkPeerPushDebit, - checkPeerPushDebitV2, - initiatePeerPushDebit, -} from "./pay-peer-push-debit.js"; -import { checkPayForTemplate } from "./pay-template.js"; -import { fillDefaults } from "./preset-exchanges.js"; -import { - ProgressContext, - handleCancelProgressToken, - handleRetryProgressTokenNow, - withMaybeProgressContext, -} from "./progress.js"; +import { ProgressContext } from "./progress.js"; import { AfterCommitInfo, DbAccess, @@ -428,58 +101,8 @@ import { TransactionAbortedError, TriggerSpec, } from "./query.js"; -import { forceRefresh } from "./refresh.js"; -import { - TaskScheduler, - TaskSchedulerImpl, - convertTaskToTransactionId, - getActiveTaskIds, -} from "./shepherd.js"; -import { - completeAliasRegistration, - lookupAlias, - registerAlias, -} from "./taldir.js"; -import { - WithdrawTestBalanceResult, - runIntegrationTest, - runIntegrationTest2, - testPay, - testingWaitBalance, - waitTasksDone, - waitTransactionState, - waitUntilAllTransactionsFinal, - waitUntilRefreshesDone, - withdrawTestBalance, -} from "./testing.js"; -import { - deleteDiscount, - deleteSubscription, - listDiscounts, - listSubscriptions, -} from "./tokenFamilies.js"; -import { - abortTransaction, - deleteTransaction, - failTransaction, - getTransactionById, - getTransactions, - getTransactionsV2, - parseTransactionIdentifier, - rematerializeTransactions, - restartAll as restartAllRunningTasks, - resumeTransaction, - retryAll, - retryTransaction, - suspendTransaction, -} from "./transactions.js"; -import { - WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION, - WALLET_COREBANK_API_PROTOCOL_VERSION, - WALLET_CORE_API_PROTOCOL_VERSION, - WALLET_EXCHANGE_PROTOCOL_VERSION, - WALLET_MERCHANT_PROTOCOL_VERSION, -} from "./versions.js"; +import { TaskScheduler, TaskSchedulerImpl } from "./shepherd.js"; +import { rematerializeTransactions } from "./transactions.js"; import { WalletApiOperation, WalletCoreApiClient, @@ -489,15 +112,7 @@ import { WalletOperations, walletApiExpectedErrors, } from "./wallet-api-types.js"; -import { - acceptBankIntegratedWithdrawal, - confirmWithdrawal, - createManualWithdrawal, - getWithdrawalDetailsForAmount, - getWithdrawalDetailsForUri, - prepareBankIntegratedWithdrawal, - updateWithdrawalDenomsForExchange, -} from "./withdraw.js"; +import { updateWithdrawalDenomsForExchange } from "./withdraw.js"; const logger = new Logger("wallet.ts"); @@ -521,2382 +136,166 @@ export interface WalletExecutionContext { */ runWalletDbTx<T>(f: (tx: WalletDbTransaction) => Promise<T>): Promise<T>; readonly ws: InternalWalletState; - readonly cryptoApi: TalerCryptoInterface; - readonly cancellationToken: CancellationToken; - readonly http: HttpRequestLibrary; - readonly db: DbAccess<typeof WalletIndexedDbStoresV1>; - readonly oc: ObservabilityContext; - readonly cts: CancellationToken.Source | undefined; - readonly taskScheduler: TaskScheduler; - readonly dbRetryState: DbRetryState; -} - -export interface DbRetryState { - retriedExchangeUpdate?: Set<string>; -} - -export function walletExchangeClient( - baseUrl: string, - wex: WalletExecutionContext, -): TalerExchangeHttpClient { - return new TalerExchangeHttpClient(baseUrl, { - httpClient: wex.http, - cancelationToken: wex.cancellationToken, - longPollQueue: wex.ws.longpollQueue, - }); -} - -export function walletMerchantClient( - baseUrl: string, - wex: WalletExecutionContext, -): TalerMerchantInstanceHttpClient { - return new TalerMerchantInstanceHttpClient( - baseUrl, - wex.http, - undefined, - wex.cancellationToken, - ); -} - -export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; -export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; - -export type NotificationListener = (n: WalletNotification) => void; - -type CancelFn = () => void; - -/** - * Incremented each time we want to re-materialize transactions. - * - * Bumped to 4 for BUG-084: after the status-enum digit fixup - * (fixup20260718StatusEnumDigits) rewrites the raw records, transactionsMeta - * and its byStatus index must be rebuilt from the corrected values. - */ -const MATERIALIZED_TRANSACTIONS_VERSION = 4; - -async function migrateMaterializedTransactions( - wex: WalletExecutionContext, -): Promise<void> { - await wex.runWalletDbTx(async (tx) => { - const ver = await tx.getConfig( - ConfigRecordKey.MaterializedTransactionsVersion, - ); - if (ver) { - if (ver.value == MATERIALIZED_TRANSACTIONS_VERSION) { - return; - } - if (ver.value > MATERIALIZED_TRANSACTIONS_VERSION) { - logger.error( - "database is newer than code (materializedTransactionsVersion)", - ); - return; - } - } - - await rematerializeTransactions(wex, tx); - - await tx.upsertConfig({ - key: ConfigRecordKey.MaterializedTransactionsVersion, - value: MATERIALIZED_TRANSACTIONS_VERSION, - }); - }); -} - -export async function getDenomInfo( - wex: WalletExecutionContext, - tx: WalletDbTransaction | LegacyWalletTxHandle, - exchangeBaseUrl: string, - denomPubHash: string, -): Promise<DenominationInfo | undefined> { - const key = `${exchangeBaseUrl}:${denomPubHash}`; - return wex.ws.denomInfoCache.getOrPut(key, async () => { - const d = - "getDenomination" in tx - ? await tx.getDenomination(exchangeBaseUrl, denomPubHash) - : await tx.wtx.getDenomination(exchangeBaseUrl, denomPubHash); - if (d != null) { - return WalletDenomination.toDenomInfo(d); - } else { - return undefined; - } - }); -} - -/** - * List bank accounts known to the wallet from - * previous withdrawals. - */ -async function handleListBankAccounts( - wex: WalletExecutionContext, - req: ListBankAccountsRequest, -): Promise<ListBankAccountsResponse> { - const accounts: WalletBankAccountInfo[] = []; - const currency = req.currency; - await wex.runWalletDbTx(async (tx) => { - const knownAccounts = await tx.listBankAccounts(); - for (const r of knownAccounts) { - if (currency && r.currencies && !r.currencies.includes(currency)) { - continue; - } - const payto = Result.orUndefined(Paytos.fromString(r.paytoUri)); - if (payto) { - accounts.push({ - bankAccountId: r.bankAccountId, - paytoUri: r.paytoUri, - label: r.label, - kycCompleted: r.kycCompleted, - currencies: r.currencies, - }); - } - } - }); - return { accounts }; -} - -async function handleGetBankAccountById( - wex: WalletExecutionContext, - req: GetBankAccountByIdRequest, -): Promise<GetBankAccountByIdResponse> { - const acct = await wex.runWalletDbTx(async (tx) => { - return tx.getBankAccount(req.bankAccountId); - }); - if (!acct) { - throw Error(`bank account ${req.bankAccountId} not found`); - } - return acct; -} - -/** - * Remove a known bank account. - */ -async function forgetBankAccount( - wex: WalletExecutionContext, - bankAccountId: string, -): Promise<void> { - await wex.runWalletDbTx(async (tx) => { - const account = await tx.getBankAccount(bankAccountId); - if (!account) { - throw Error(`account not found: ${bankAccountId}`); - } - await tx.deleteBankAccount(account.bankAccountId); - }); - wex.ws.notify({ - type: NotificationType.BankAccountChange, - bankAccountId, - }); - return; -} - -async function setCoinSuspended( - wex: WalletExecutionContext, - coinPub: string, - suspended: boolean, -): Promise<void> { - await wex.runWalletDbTx(async (tx) => { - const c = await tx.getCoin(coinPub); - if (!c) { - logger.warn(`coin ${coinPub} not found, won't suspend`); - return; - } - const coinAvailability = await tx.getCoinAvailability( - c.exchangeBaseUrl, - c.denomPubHash, - c.maxAge, - ); - checkDbInvariant( - !!coinAvailability, - `no denom info for ${c.denomPubHash} age ${c.maxAge}`, - ); - if (suspended) { - if (c.status !== CoinStatus.Fresh) { - return; - } - if (coinAvailability.freshCoinCount === 0) { - throw Error( - `invalid coin count ${coinAvailability.freshCoinCount} in DB`, - ); - } - coinAvailability.freshCoinCount--; - c.status = CoinStatus.FreshSuspended; - } else { - if (c.status == CoinStatus.Dormant) { - return; - } - coinAvailability.freshCoinCount++; - c.status = CoinStatus.Fresh; - } - await tx.upsertCoin(c); - await tx.upsertCoinAvailability(coinAvailability); - }); -} - -/** - * Dump the public information of coins we have in an easy-to-process format. - */ -async function dumpCoins(wex: WalletExecutionContext): Promise<CoinDumpJson> { - const coinsJson: CoinDumpJson = { coins: [] }; - logger.info("dumping coins"); - await wex.runWalletDbTx(async (tx) => { - const coins = await tx.listAllCoins(); - for (const c of coins) { - const denom = await tx.getDenomination(c.exchangeBaseUrl, c.denomPubHash); - if (!denom) { - logger.warn("no denom 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) { - withdrawalReservePub = cs.reservePub; - } - const denomInfo = await getDenomInfo( - wex, - tx, - c.exchangeBaseUrl, - c.denomPubHash, - ); - if (!denomInfo) { - logger.warn("no denomination found for coin"); - continue; - } - const historyRec = await tx.getCoinHistory(c.coinPub); - coinsJson.coins.push({ - coinPub: c.coinPub, - denomPub: denom.denomPub, - denomPubHash: c.denomPubHash, - denomValue: denom.value, - exchangeBaseUrl: c.exchangeBaseUrl, - refreshParentCoinPub: refreshParentCoinPub, - withdrawalReservePub: withdrawalReservePub, - coinStatus: c.status, - ageCommitmentProof: c.ageCommitmentProof, - history: historyRec ? historyRec.history : [], - }); - } - }); - return coinsJson; -} - -/** - * Get an API client from an internal wallet state object. - */ -let id = 0; -async function getClientFromWalletState( - ws: InternalWalletState, -): Promise<WalletCoreApiClient> { - const client: WalletCoreApiClient = { - async call(op, payload): Promise<any> { - id = (id + 1) % (Number.MAX_SAFE_INTEGER - 100); - const res = await dispatchWalletCoreApiRequest( - ws, - op, - String(id), - payload, - ); - switch (res.type) { - case "error": - throw TalerError.fromUncheckedDetail(res.error); - case "response": - return res.result; - } - }, - callForResult: async function <Op extends keyof WalletOperations>( - op: Op, - payload: WalletCoreRequestType<Op>, - ): Promise<Result<WalletCoreResponseType<Op>, WalletCoreErrorType<Op>>> { - id = (id + 1) % (Number.MAX_SAFE_INTEGER - 100); - const res = await dispatchWalletCoreApiRequest( - ws, - op, - String(id), - payload, - ); - switch (res.type) { - case "error": { - if (op in walletApiExpectedErrors) { - const errs = ( - walletApiExpectedErrors as { - [x: string]: readonly TalerErrorCode[]; - } - )[op]; - if (errs.includes(res.error.code)) { - return Result.errorWithDetail( - res.error.code as WalletCoreErrorType<Op>, - res.error, - ); - } - } - throw TalerError.fromUncheckedDetail(res.error); - } - case "response": - return Result.of(res.result as WalletCoreResponseType<Op>); - default: - assertUnreachable(res); - } - }, - }; - return client; -} - -async function createStoredBackup( - wex: WalletExecutionContext, -): Promise<CreateStoredBackupResponse> { - const backup = await exportDb(wex.ws.idbFactory); - const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); - const name = `backup-${new Date().getTime()}`; - await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { - await tx.backupMeta.add({ - name, - }); - await tx.backupData.add(backup, name); - }); - return { - name, - }; -} - -async function listStoredBackups( - wex: WalletExecutionContext, -): Promise<StoredBackupList> { - const storedBackups: StoredBackupList = { - storedBackups: [], - }; - const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); - await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { - await tx.backupMeta.iter().forEach((x) => { - storedBackups.storedBackups.push({ - name: x.name, - }); - }); - }); - return storedBackups; -} - -async function deleteStoredBackup( - wex: WalletExecutionContext, - req: DeleteStoredBackupRequest, -): Promise<void> { - const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); - await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { - await tx.backupData.delete(req.name); - await tx.backupMeta.delete(req.name); - }); -} - -async function recoverStoredBackup( - wex: WalletExecutionContext, - req: RecoverStoredBackupRequest, -): Promise<void> { - logger.info(`Recovering stored backup ${req.name}`); - const { name } = req; - const backupsDb = await openStoredBackupsDatabase(wex.ws.idbFactory); - const bd = await backupsDb.runAllStoresReadWriteTx({}, async (tx) => { - const backupMeta = await tx.backupMeta.get(name); - if (!backupMeta) { - throw Error("backup not found"); - } - const backupData = await tx.backupData.get(name); - if (!backupData) { - throw Error("no backup data (DB corrupt)"); - } - return backupData; - }); - logger.info(`backup found, now importing`); - await importDb(wex.db.idbHandle(), bd); - await wex.runLegacyWalletDbTx(async (tx) => { - await rematerializeTransactions(wex, tx.wtx); - // Clear fixups. Okay since they are idempotent. - const fixups = await tx.fixups.getAll(); - for (const f of fixups) { - await tx.fixups.delete(f.fixupName); - } - }); - logger.info(`import done`); -} - -async function handlePrepareWithdrawExchange( - wex: WalletExecutionContext, - req: PrepareWithdrawExchangeRequest, -): Promise<PrepareWithdrawExchangeResponse> { - const parsedUri = Result.orUndefined(TalerUris.parse(req.talerUri)); - if (parsedUri?.type !== TalerUriAction.WithdrawExchange) { - throw Error("expected a taler://withdraw-exchange URI"); - } - const exchangeBaseUrl = parsedUri.exchangeBaseUrl; - - return await withMaybeProgressContext( - wex, - "prepareWithdrawExchange", - req.progressToken, - async (pc) => { - if (pc) { - pc.onRetryNow = async () => { - await resetExchangeRetries(wex, exchangeBaseUrl); - }; - } - const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, { - noBail: pc != null, - progressContext: pc, - }); - if (parsedUri.amount) { - const amt = Amounts.parseOrThrow(parsedUri.amount); - if (amt.currency !== exchange.currency) { - throw Error("mismatch of currency (URI vs exchange)"); - } - } - return { - exchangeBaseUrl, - amount: parsedUri.amount, - }; - }, - ); -} - -async function handleSharePayment( - wex: WalletExecutionContext, - req: SharePaymentRequest, -): Promise<SharePaymentResult> { - return await sharePayment(wex, req.merchantBaseUrl, req.orderId); -} - -async function handleDeleteStoredBackup( - wex: WalletExecutionContext, - req: DeleteStoredBackupRequest, -): Promise<EmptyObject> { - await deleteStoredBackup(wex, req); - return {}; -} - -async function handleRecoverStoredBackup( - wex: WalletExecutionContext, - req: RecoverStoredBackupRequest, -): Promise<EmptyObject> { - await recoverStoredBackup(wex, req); - return {}; -} - -const urlCharRegex = /^[a-zA-Z0-9\-_.~!*'();:@&=+$,/?%#[\]]+$/; - -export async function handleCompleteExchangeBaseUrl( - wex: WalletExecutionContext, - req: CompleteBaseUrlRequest, -): Promise<CompleteBaseUrlResult> { - const trimmedUrl = req.url.trim(); - - if (!urlCharRegex.test(trimmedUrl)) { - return { - status: "bad-syntax", - error: { - code: TalerErrorCode.WALLET_CORE_API_BAD_REQUEST, - }, - }; - } - - // FIXME: Do completion via network. - return { - completion: canonicalizeBaseUrl(trimmedUrl), - status: "ok", - }; -} - -async function handleSetWalletRunConfig( - wex: WalletExecutionContext, - req: InitRequest, -) { - if (logger.shouldLogTrace()) { - const initType = wex.ws.initCalled - ? "repeat initialization" - : "first initialization"; - logger.trace(`init request (${initType}): ${j2s(req)}`); - } - - // Write to the DB to make sure that we're failing early in - // case the DB is not writeable. - try { - await wex.runWalletDbTx(async (tx) => { - tx.upsertConfig({ - key: ConfigRecordKey.LastInitInfo, - value: timestampProtocolToDb(TalerProtocolTimestamp.now()), - }); - }); - } catch (e) { - logger.error( - "error writing to database during initialization (while writing last error info)", - ); - const err = getErrorDetailFromException(e); - logger.error(`details: ${j2s(err)}`); - throw TalerError.fromDetail(TalerErrorCode.WALLET_DB_UNAVAILABLE, { - innerError: err, - }); - } - wex.ws.initWithConfig(applyRunConfigDefaults(req.config)); - - if (wex.ws.config.testing.skipDefaults) { - logger.trace("skipping defaults"); - } else { - logger.trace("filling defaults"); - await fillDefaults(wex); - } - - if (req.config?.logLevel) { - setGlobalLogLevelFromString(req.config.logLevel); - } - - await migrateMaterializedTransactions(wex); - - const resp: InitResponse = { - versionInfo: await handleGetVersion(wex), - }; - - if (req.config?.lazyTaskLoop) { - logger.trace("lazily starting task loop"); - } else { - await wex.taskScheduler.ensureRunning(); - } - - wex.ws.initCalled = true; - return resp; -} - -async function handleWithdrawTestkudos(wex: WalletExecutionContext) { - return await withdrawTestBalance(wex, { - amount: "TESTKUDOS:10" as AmountString, - corebankApiBaseUrl: "https://bank.test.taler.net/", - exchangeBaseUrl: "https://exchange.test.taler.net/", - }); -} - -async function handleWithdrawTestBalance( - wex: WalletExecutionContext, - req: WithdrawTestBalanceRequest, -): Promise<WithdrawTestBalanceResult> { - return await withdrawTestBalance(wex, req); -} - -async function handleRunIntegrationTest( - wex: WalletExecutionContext, - req: IntegrationTestArgs, -): Promise<EmptyObject> { - await runIntegrationTest(wex, req); - return {}; -} - -async function handleRunIntegrationTestV2( - wex: WalletExecutionContext, - req: IntegrationTestV2Args, -): Promise<EmptyObject> { - await runIntegrationTest2(wex, req); - return {}; -} - -async function handleValidateIban( - wex: WalletExecutionContext, - req: ValidateIbanRequest, -): Promise<ValidateIbanResponse> { - const valRes = validateIban(req.iban); - const resp: ValidateIbanResponse = { - valid: valRes.type === "valid", - }; - return resp; -} - -async function handleAddExchange( - wex: WalletExecutionContext, - req: AddExchangeRequest, -): Promise<AddExchangeResponse> { - let exchangeBaseUrl: string; - if (req.exchangeBaseUrl) { - logger.warn( - "Deprecated request property: AddExchangeRequest.exchangeBaseUrl", - ); - exchangeBaseUrl = req.exchangeBaseUrl; - } else if (req.uri) { - if (req.uri.startsWith("taler")) { - const p = Result.orUndefined(TalerUris.parse(req.uri)); - if (p?.type !== TalerUriAction.AddExchange) { - throw Error("invalid taler://add-exchange/ URI"); - } - exchangeBaseUrl = p.exchangeBaseUrl; - } else if (req.allowCompletion) { - const completeRes = await handleCompleteExchangeBaseUrl(wex, { - url: req.uri, - }); - if (completeRes.status != "ok") { - throw TalerError.fromUncheckedDetail(completeRes.error); - } - exchangeBaseUrl = completeRes.completion; - } else if (req.uri.startsWith("http")) { - const canonUrl = canonicalizeBaseUrl(req.uri); - if (req.uri != canonUrl) { - throw Error("exchange base URL must be canonicalized"); - } - exchangeBaseUrl = req.uri; - } else { - throw Error("AddExchangeRequest.uri must be http(s):// or taler://"); - } - } else { - throw Error( - "AddExchangeRequest must either specify uri or exchangeBaseUrl", - ); - } - - // FIXME: Check /config before adding the exchange entry. - - // FIXME: We probably should not wait synchronously here. - await fetchFreshExchange(wex, exchangeBaseUrl, {}); - // Exchange has been explicitly added upon user request. - // Thus, we mark it as "used". - if (!req.ephemeral) { - await wex.runWalletDbTx(async (tx) => { - await markExchangeUsed(tx, exchangeBaseUrl); - }); - } - return { - exchangeBaseUrl, - }; -} - -async function handleUpdateExchangeEntry( - wex: WalletExecutionContext, - req: UpdateExchangeEntryRequest, -): Promise<EmptyObject> { - await startUpdateExchangeEntry(wex, req.exchangeBaseUrl, { - forceUpdate: !!req.force, - forceUnavailable: !!req.force, - }); - return {}; -} - -async function handleTestingGetDenomStats( - wex: WalletExecutionContext, - req: TestingGetDenomStatsRequest, -): Promise<TestingGetDenomStatsResponse> { - const denomStats: TestingGetDenomStatsResponse = { - numKnown: 0, - numLost: 0, - numOffered: 0, - }; - await wex.runWalletDbTx(async (tx) => { - const denoms = await tx.getDenominationsByExchange(req.exchangeBaseUrl); - for (const d of denoms) { - denomStats.numKnown++; - if (d.isOffered) { - denomStats.numOffered++; - } - if (d.isLost) { - denomStats.numLost++; - } - } - }); - return denomStats; -} - -async function handleAddBankAccount( - wex: WalletExecutionContext, - req: AddBankAccountRequest, -): Promise<AddBankAccountResponse> { - if (Result.isError(Paytos.fromString(req.paytoUri))) { - throw Error("invalid payto"); - } - const acctId = await wex.runWalletDbTx(async (tx) => { - let currencies = req.currencies; - let myId: string; - const oldAcct = await tx.getBankAccountByPaytoUri(req.paytoUri); - if (req.replaceBankAccountId) { - myId = req.replaceBankAccountId; - } else if (oldAcct) { - myId = oldAcct.bankAccountId; - currencies = [ - ...new Set([...(req.currencies ?? []), ...(oldAcct.currencies ?? [])]), - ]; - currencies.sort(); - } else { - // New Account! - myId = `acct:${encodeCrock(getRandomBytes(32))}`; - } - await tx.upsertBankAccount({ - bankAccountId: myId, - paytoUri: req.paytoUri, - label: req.label, - currencies, - kycCompleted: false, - }); - return myId; - }); - wex.ws.notify({ - type: NotificationType.BankAccountChange, - bankAccountId: acctId, - }); - return { - bankAccountId: acctId, - }; -} - -async function handleForgetBankAccount( - wex: WalletExecutionContext, - req: ForgetBankAccountRequest, -): Promise<EmptyObject> { - await forgetBankAccount(wex, req.bankAccountId); - return {}; -} - -// FIXME: Doesn't have proper type! -async function handleTestingGetReserveHistory( - wex: WalletExecutionContext, - req: TestingGetReserveHistoryRequest, -): Promise<any> { - const reserve = await wex.runWalletDbTx(async (tx) => { - return tx.getReserveByReservePub(req.reservePub); - }); - if (!reserve) { - throw Error("no reserve pub found"); - } - const sigResp = await wex.cryptoApi.signReserveHistoryReq({ - reservePriv: reserve.reservePriv, - startOffset: 0, - }); - const exchangeBaseUrl = req.exchangeBaseUrl; - const url = new URL(`reserves/${req.reservePub}/history`, exchangeBaseUrl); - const resp = await cancelableFetch(wex, url, { - headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, - }); - const historyJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); - return historyJson; -} - -async function handleAcceptManualWithdrawal( - wex: WalletExecutionContext, - req: AcceptManualWithdrawalRequest, -): Promise<AcceptManualWithdrawalResult> { - const res = await createManualWithdrawal(wex, { - amount: Amounts.parseOrThrow(req.amount), - exchangeBaseUrl: req.exchangeBaseUrl, - restrictAge: req.restrictAge, - forceReservePriv: req.forceReservePriv, - }); - return res; -} - -async function handleGetExchangeTos( - wex: WalletExecutionContext, - req: GetExchangeTosRequest, -): Promise<GetExchangeTosResult> { - return await withMaybeProgressContext( - wex, - "getExchangeTos", - req.progressToken, - async (pc) => { - return getExchangeTos( - wex, - req.exchangeBaseUrl, - req.acceptedFormat, - req.acceptLanguage, - pc, - ); - }, - ); -} - -async function handleGetQrCodesForPayto( - wex: WalletExecutionContext, - req: GetQrCodesForPaytoRequest, -): Promise<GetQrCodesForPaytoResponse> { - return { - codes: getQrCodesForPayto(req.paytoUri), - }; -} - -async function handleGetBankingChoicesForPayto( - wex: WalletExecutionContext, - req: GetBankingChoicesForPaytoRequest, -): Promise<GetBankingChoicesForPaytoResponse> { - const parsedPayto = Result.orUndefined(Paytos.fromString(req.paytoUri)); - if (!parsedPayto) { - throw Error("invalid payto URI"); - } - const amount = parsedPayto.params["amount"]; - if (!amount) { - logger.warn("payto URI has no amount"); - return { - choices: [], - }; - } - const currency = Amounts.currencyOf(amount); - switch (currency) { - case "KUDOS": - return { - choices: [ - { - label: "Demobank Website", - type: "link", - uri: `https://bank.demo.taler.net/webui/#/transfer/${encodeURIComponent( - req.paytoUri, - )}`, - }, - { - label: "Demobank App", - type: "link", - uri: `https://bank.demo.taler.net/app/transfer/${encodeURIComponent( - req.paytoUri, - )}`, - }, - ], - }; - break; - default: - return { - choices: [], - }; - } -} - -async function handleGetChoicesForPayment( - wex: WalletExecutionContext, - req: GetChoicesForPaymentRequest, -): Promise<GetChoicesForPaymentResult> { - return await getChoicesForPayment(wex, req.transactionId, req.forcedCoinSel); -} - -async function handleConfirmPay( - wex: WalletExecutionContext, - req: ConfirmPayRequest, -): Promise<ConfirmPayResult> { - return await confirmPay(wex, { - transactionId: req.transactionId, - choiceIndex: req.choiceIndex, - forcedCoinSel: undefined, - sessionIdOverride: req.sessionId, - useDonau: req.useDonau, - noWait: req.noWait ?? wex.ws.devExperimentState.flagConfirmPayNoWait, - }); -} - -async function handleListDiscounts( - wex: WalletExecutionContext, - req: ListDiscountsRequest, -): Promise<ListDiscountsResponse> { - return await listDiscounts(wex, req.tokenIssuePubHash, req.merchantBaseUrl); -} - -async function handleDeleteDiscount( - wex: WalletExecutionContext, - req: DeleteDiscountRequest, -): Promise<EmptyObject> { - return await deleteDiscount(wex, req.tokenFamilyHash); -} - -async function handleListSubscriptions( - wex: WalletExecutionContext, - req: ListSubscriptionsRequest, -): Promise<ListSubscriptionsResponse> { - return await listSubscriptions( - wex, - req.tokenIssuePubHash, - req.merchantBaseUrl, - ); -} - -async function handleDeleteSubscription( - wex: WalletExecutionContext, - req: DeleteSubscriptionRequest, -): Promise<EmptyObject> { - return await deleteSubscription(wex, req.tokenFamilyHash); -} - -async function handleAbortTransaction( - wex: WalletExecutionContext, - req: AbortTransactionRequest, -): Promise<EmptyObject> { - await abortTransaction(wex, req.transactionId); - return {}; -} - -async function handleSuspendTransaction( - wex: WalletExecutionContext, - req: SuspendTransactionRequest, -): Promise<EmptyObject> { - await suspendTransaction(wex, req.transactionId); - return {}; -} - -async function handleGetActiveTasks( - wex: WalletExecutionContext, - req: EmptyObject, -): Promise<GetActiveTasksResponse> { - const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds; - - const tasksInfo = await Promise.all( - allTasksId.map(async (id) => { - return await wex.runWalletDbTx(async (tx) => { - return tx.getOperationRetry(id); - }); - }), - ); - - const tasks = allTasksId.map((taskId, i): ActiveTask => { - const transaction = convertTaskToTransactionId(taskId); - const d = tasksInfo[i]; - - const firstTry = !d - ? undefined - : timestampAbsoluteFromDb(d.retryInfo.firstTry); - const nextTry = !d - ? undefined - : timestampAbsoluteFromDb(d.retryInfo.nextRetry); - const counter = d?.retryInfo.retryCounter; - const lastError = d?.lastError; - - return { - taskId: taskId, - retryCounter: counter, - firstTry, - nextTry, - lastError, - transaction, - }; - }); - return { tasks }; -} - -async function handleFailTransaction( - wex: WalletExecutionContext, - req: FailTransactionRequest, -): Promise<EmptyObject> { - await failTransaction(wex, req.transactionId); - return {}; -} - -async function handleTestingGetSampleTransactions( - wex: WalletExecutionContext, - req: EmptyObject, -): Promise<TransactionsResponse> { - // FIXME! - return { transactions: [] }; - // These are out of date! - //return { transactions: sampleWalletCoreTransactions }; -} - -async function handleStartRefundQuery( - wex: WalletExecutionContext, - req: StartRefundQueryRequest, -): Promise<EmptyObject> { - const txIdParsed = parseTransactionIdentifier(req.transactionId); - if (!txIdParsed) { - throw Error("invalid transaction ID"); - } - if (txIdParsed.tag !== TransactionType.Payment) { - throw Error("expected payment transaction ID"); - } - await startQueryRefund(wex, txIdParsed.proposalId); - return {}; -} - -async function handleHintNetworkAvailability( - wex: WalletExecutionContext, - req: HintNetworkAvailabilityRequest, -): Promise<EmptyObject> { - // If network was already available, don't do anything - if (wex.ws.networkAvailable === req.isNetworkAvailable) { - return {}; - } - wex.ws.networkAvailable = req.isNetworkAvailable; - // When network becomes available, restart tasks as they're blocked - // waiting for the network. - // When network goes down, restart tasks so they notice the network - // is down and wait. - await restartAllRunningTasks(wex); - return {}; -} - -async function handleGetDepositWireTypes( - wex: WalletExecutionContext, - req: GetDepositWireTypesRequest, -): Promise<GetDepositWireTypesResponse> { - const wtSet: Set<string> = new Set(); - const wireTypeDetails: WireTypeDetails[] = []; - const talerBankHostnames: string[] = []; - await wex.runWalletDbTx(async (tx) => { - const exchanges = await tx.getExchanges(); - for (const exchange of exchanges) { - const det = await getExchangeDetailsInTx(tx, exchange.baseUrl); - if (!det) { - continue; - } - if (req.currency !== undefined && det.currency !== req.currency) { - continue; - } - for (const acc of det.wireInfo.accounts) { - let usable = true; - for (const dr of acc.debit_restrictions) { - if (dr.type === "deny") { - usable = false; - break; - } - } - if (!usable) { - continue; - } - const parsedPayto = Result.orUndefined( - Paytos.fromString(acc.payto_uri), - ); - if (!parsedPayto) { - continue; - } - let preferredEntryType: "iban" | "bban" | undefined = undefined; - if (parsedPayto.targetType === PaytoType.IBAN) { - if (det.currency === "HUF") { - preferredEntryType = "bban"; - } else { - preferredEntryType = "iban"; - } - } - if (parsedPayto.targetType === PaytoType.TalerBank) { - if (!talerBankHostnames.includes(parsedPayto.host)) { - talerBankHostnames.push(parsedPayto.host); - } - } - if (!wtSet.has(parsedPayto.targetType!)) { - wtSet.add(parsedPayto.targetType!); - wireTypeDetails.push({ - paymentTargetType: parsedPayto.targetType!, - // Will possibly extended later by other exchanges - // with the same wire type. - talerBankHostnames, - preferredEntryType, - }); - } - } - } - }); - return { - wireTypeDetails, - }; -} - -async function handleGetDepositWireTypesForCurrency( - wex: WalletExecutionContext, - req: GetDepositWireTypesForCurrencyRequest, -): Promise<GetDepositWireTypesForCurrencyResponse> { - const wtSet: Set<string> = new Set(); - const wireTypeDetails: WireTypeDetails[] = []; - const talerBankHostnames: string[] = []; - await wex.runWalletDbTx(async (tx) => { - const exchanges = await tx.getExchanges(); - for (const exchange of exchanges) { - const det = await getExchangeDetailsInTx(tx, exchange.baseUrl); - if (!det) { - continue; - } - if (det.currency !== req.currency) { - continue; - } - for (const acc of det.wireInfo.accounts) { - let usable = true; - for (const dr of acc.debit_restrictions) { - if (dr.type === "deny") { - usable = false; - break; - } - } - if (!usable) { - continue; - } - const parsedPayto = Result.orUndefined( - Paytos.fromString(acc.payto_uri), - ); - if (!parsedPayto) { - continue; - } - if (parsedPayto.targetType === PaytoType.TalerBank) { - if (!talerBankHostnames.includes(parsedPayto.host)) { - talerBankHostnames.push(parsedPayto.host); - } - } - if (!wtSet.has(parsedPayto.targetType!)) { - wtSet.add(parsedPayto.targetType!); - wireTypeDetails.push({ - paymentTargetType: parsedPayto.targetType!, - // Will possibly extended later by other exchanges - // with the same wire type. - talerBankHostnames, - }); - } - } - } - }); - return { - wireTypes: [...wtSet], - wireTypeDetails, - }; -} - -async function handleListGlobalCurrencyExchanges( - wex: WalletExecutionContext, - _req: EmptyObject, -): Promise<ListGlobalCurrencyExchangesResponse> { - const resp: ListGlobalCurrencyExchangesResponse = { - exchanges: [], - }; - await wex.runWalletDbTx(async (tx) => { - const gceList = await tx.listGlobalCurrencyExchanges(); - for (const gce of gceList) { - resp.exchanges.push({ - currency: gce.currency, - exchangeBaseUrl: gce.exchangeBaseUrl, - exchangeMasterPub: gce.exchangeMasterPub, - }); - } - }); - return resp; -} - -async function handleListGlobalCurrencyAuditors( - wex: WalletExecutionContext, - _req: EmptyObject, -): Promise<ListGlobalCurrencyAuditorsResponse> { - const resp: ListGlobalCurrencyAuditorsResponse = { - auditors: [], - }; - await wex.runWalletDbTx(async (tx) => { - const gcaList = await tx.listGlobalCurrencyAuditors(); - for (const gca of gcaList) { - resp.auditors.push({ - currency: gca.currency, - auditorBaseUrl: gca.auditorBaseUrl, - auditorPub: gca.auditorPub, - }); - } - }); - return resp; -} - -export async function handleAddGlobalCurrencyExchange( - wex: WalletExecutionContext, - req: AddGlobalCurrencyExchangeRequest, -): Promise<EmptyObject> { - await wex.runWalletDbTx(async (tx) => { - const existingRec = await tx.getGlobalCurrencyExchange( - req.currency, - req.exchangeBaseUrl, - req.exchangeMasterPub, - ); - if (existingRec) { - return; - } - wex.ws.exchangeCache.clear(); - // Re-scope currency info from exchange scope to global scope. The scope - // string is the primary key, so this writes a new record and leaves the - // exchange-scoped one, as it did before. - const infoRec = await tx.getCurrencyInfo({ - type: ScopeType.Exchange, - currency: req.currency, - url: req.exchangeBaseUrl, - }); - if (infoRec) { - await tx.upsertCurrencyInfo({ - scopeInfo: { type: ScopeType.Global, currency: req.currency }, - currencySpec: infoRec.currencySpec, - source: infoRec.source, - }); - } - await tx.addGlobalCurrencyExchange({ - currency: req.currency, - exchangeBaseUrl: req.exchangeBaseUrl, - exchangeMasterPub: req.exchangeMasterPub, - }); - }); - return {}; -} - -async function handleRemoveGlobalCurrencyAuditor( - wex: WalletExecutionContext, - req: RemoveGlobalCurrencyAuditorRequest, -): Promise<EmptyObject> { - await wex.runWalletDbTx(async (tx) => { - const existingRec = await tx.getGlobalCurrencyAuditor( - req.currency, - req.auditorBaseUrl, - req.auditorPub, - ); - if (!existingRec) { - return; - } - checkDbInvariant( - !!existingRec.id, - `no global currency for ${req.currency}`, - ); - await tx.deleteGlobalCurrencyAuditor(existingRec.id); - wex.ws.exchangeCache.clear(); - }); - return {}; -} - -export async function handleRemoveGlobalCurrencyExchange( - wex: WalletExecutionContext, - req: RemoveGlobalCurrencyExchangeRequest, -): Promise<EmptyObject> { - await wex.runWalletDbTx(async (tx) => { - const existingRec = await tx.getGlobalCurrencyExchange( - req.currency, - req.exchangeBaseUrl, - req.exchangeMasterPub, - ); - if (!existingRec) { - return; - } - wex.ws.exchangeCache.clear(); - checkDbInvariant( - !!existingRec.id, - `no global exchange for ${req.currency}`, - ); - await tx.deleteCurrencyInfo({ - type: ScopeType.Global, - currency: req.currency, - }); - await tx.deleteGlobalCurrencyExchange(existingRec.id); - }); - return {}; -} - -async function handleAddGlobalCurrencyAuditor( - wex: WalletExecutionContext, - req: AddGlobalCurrencyAuditorRequest, -): Promise<EmptyObject> { - await wex.runWalletDbTx(async (tx) => { - const existingRec = await tx.getGlobalCurrencyAuditor( - req.currency, - req.auditorBaseUrl, - req.auditorPub, - ); - if (existingRec) { - return; - } - await tx.addGlobalCurrencyAuditor({ - currency: req.currency, - auditorBaseUrl: req.auditorBaseUrl, - auditorPub: req.auditorPub, - }); - wex.ws.exchangeCache.clear(); - }); - return {}; -} - -async function handleShutdown( - wex: WalletExecutionContext, - _req: EmptyObject, -): Promise<EmptyObject> { - logger.info(`Shutdown requested`); - wex.ws.stop(); - return {}; -} - -async function handleTestingSetTimetravel( - wex: WalletExecutionContext, - req: TestingSetTimetravelRequest, -): Promise<EmptyObject> { - setDangerousTimetravel(req.offsetMs); - await wex.taskScheduler.reload(); - return {}; -} - -async function handleCanonicalizeBaseUrl( - _wex: WalletExecutionContext, - req: CanonicalizeBaseUrlRequest, -): Promise<CanonicalizeBaseUrlResponse> { - return { - url: canonicalizeBaseUrl(req.url), - }; -} - -async function handleDeleteExchange( - wex: WalletExecutionContext, - req: DeleteExchangeRequest, -): Promise<EmptyObject> { - await deleteExchange(wex, req); - return {}; -} - -async function handleCreateStoredBackup( - wex: WalletExecutionContext, - _req: EmptyObject, -): Promise<CreateStoredBackupResponse> { - return await createStoredBackup(wex); -} - -async function handleExportDbToFile( - wex: WalletExecutionContext, - req: ExportDbToFileRequest, -): Promise<ExportDbToFileResponse> { - const res = await wex.ws.dbImplementation.exportToFile( - req.directory, - req.stem, - req.forceFormat, - ); - return { - path: res.path, - }; -} - -async function handleImportDb( - wex: WalletExecutionContext, - req: ImportDbRequest, -): Promise<EmptyObject> { - // FIXME: This should atomically re-materialize transactions! - await importDb(wex.db.idbHandle(), req.dump); - await wex.runLegacyWalletDbTx(async (tx) => { - await rematerializeTransactions(wex, tx.wtx); - // Clear fixups. Okay since they are idempotent. - const fixups = await tx.fixups.getAll(); - for (const f of fixups) { - await tx.fixups.delete(f.fixupName); - } - }); - return {}; -} - -async function handleImportDbFromFile( - wex: WalletExecutionContext, - req: ImportDbFromFileRequest, -): Promise<EmptyObject> { - if (req.path.endsWith(".json")) { - const dump = await wex.ws.dbImplementation.readBackupJson(req.path); - return await handleImportDb(wex, { - dump, - }); - } else { - throw Error("DB file import only supports .json files at the moment"); - } -} - -async function handleAcceptBankIntegratedWithdrawal( - wex: WalletExecutionContext, - req: AcceptBankIntegratedWithdrawalRequest, -): Promise<AcceptWithdrawalResponse> { - return await acceptBankIntegratedWithdrawal(wex, { - selectedExchange: req.exchangeBaseUrl, - talerWithdrawUri: req.talerWithdrawUri, - forcedDenomSel: req.forcedDenomSel, - restrictAge: req.restrictAge, - amount: req.amount, - }); -} - -async function handleGetCurrencySpecification( - wex: WalletExecutionContext, - req: GetCurrencySpecificationRequest, -): Promise<GetCurrencySpecificationResponse> { - const spec = await wex.runWalletDbTx(async (tx) => { - return tx.getCurrencyInfo(req.scope); - }); - if (spec) { - if ( - wex.ws.devExperimentState.fakeDemoShortcuts != null && - req.scope.type === ScopeType.Exchange && - req.scope.url === "https://exchange.demo.taler.net/" - ) { - spec.currencySpec.common_amounts = - wex.ws.devExperimentState.fakeDemoShortcuts; - } - return { - currencySpecification: spec.currencySpec, - }; - } - // Hard-coded mock for KUDOS and TESTKUDOS - if (req.scope.currency === "KUDOS") { - const kudosResp: GetCurrencySpecificationResponse = { - currencySpecification: { - name: "Kudos (Taler Demonstrator)", - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": "ク", - }, - }, - }; - return kudosResp; - } else if (req.scope.currency === "TESTKUDOS") { - const testkudosResp: GetCurrencySpecificationResponse = { - currencySpecification: { - name: "Test (Taler Unstable Demonstrator)", - num_fractional_input_digits: 0, - num_fractional_normal_digits: 0, - num_fractional_trailing_zero_digits: 0, - alt_unit_names: { - "0": "テ", - }, - }, - }; - return testkudosResp; - } - const defaultResp: GetCurrencySpecificationResponse = { - currencySpecification: { - name: req.scope.currency, - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": req.scope.currency, - }, - }, - }; - return defaultResp; + readonly cryptoApi: TalerCryptoInterface; + readonly cancellationToken: CancellationToken; + readonly http: HttpRequestLibrary; + readonly db: DbAccess<typeof WalletIndexedDbStoresV1>; + readonly oc: ObservabilityContext; + readonly cts: CancellationToken.Source | undefined; + readonly taskScheduler: TaskScheduler; + readonly dbRetryState: DbRetryState; } -export async function handleHintApplicationResumed( - wex: WalletExecutionContext, - req: EmptyObject, -): Promise<EmptyObject> { - logger.info("handling hintApplicationResumed"); - await restartAllRunningTasks(wex); - return {}; +export interface DbRetryState { + retriedExchangeUpdate?: Set<string>; } -export async function handleTestingRunFixup( +export function walletExchangeClient( + baseUrl: string, wex: WalletExecutionContext, - req: RunFixupRequest, -): Promise<EmptyObject> { - for (const fixup of walletDbFixups) { - if (fixup.name != req.id) { - continue; - } - await wex.runLegacyWalletDbTx(async (tx) => { - await fixup.fn(tx); - await rematerializeTransactions(wex, tx.wtx); - }); - return {}; - } - throw Error("fixup not found"); +): TalerExchangeHttpClient { + return new TalerExchangeHttpClient(baseUrl, { + httpClient: wex.http, + cancelationToken: wex.cancellationToken, + longPollQueue: wex.ws.longpollQueue, + }); } -async function handleGetVersion( +export function walletMerchantClient( + baseUrl: string, wex: WalletExecutionContext, -): Promise<WalletCoreVersion> { - const result: WalletCoreVersion = { - implementationSemver: walletCoreBuildInfo.implementationSemver, - implementationGitHash: walletCoreBuildInfo.implementationGitHash, - hash: undefined, - version: WALLET_CORE_API_PROTOCOL_VERSION, - exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, - merchant: WALLET_MERCHANT_PROTOCOL_VERSION, - bankConversionApiRange: WALLET_BANK_CONVERSION_API_PROTOCOL_VERSION, - bankIntegrationApiRange: TalerBankIntegrationHttpClient.PROTOCOL_VERSION, - corebankApiRange: WALLET_COREBANK_API_PROTOCOL_VERSION, - bank: TalerBankIntegrationHttpClient.PROTOCOL_VERSION, - devMode: wex.ws.config.testing.devModeActive, - }; - return result; +): TalerMerchantInstanceHttpClient { + return new TalerMerchantInstanceHttpClient( + baseUrl, + wex.http, + undefined, + wex.cancellationToken, + ); } -export async function handleConvertIbanAccountFieldToPayto( - wex: WalletExecutionContext, - req: ConvertIbanAccountFieldToPaytoRequest, -): Promise<ConvertIbanAccountFieldToPaytoResponse> { - const strippedInput = req.value.replace(/[- ]/g, ""); - if (req.currency === "HUF") { - const iban = convertHUF_BBANtoIBAN(strippedInput); - if (Result.isOk(iban)) { - return { - ok: true, - paytoUri: `payto://iban/${iban.value}`, - type: "iban", - }; - } else { - return { - ok: false, - }; - } - } - if (wex.ws.devExperimentState.fakeChfBban && req.currency === "CHF") { - const iban = convertCHF_BBANtoIBAN(strippedInput); - if (Result.isOk(iban)) { - return { - ok: true, - paytoUri: `payto://iban/${iban.value}`, - type: "iban", - }; - } else { - return { - ok: false, - }; - } - } - const parsedIban = parseIban(strippedInput); - if (Result.isOk(parsedIban)) { - return { - ok: true, - paytoUri: `payto://iban/${strippedInput}`, - type: "iban", - }; - } else { - return { - ok: false, - }; - } -} +export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; +export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; -export async function handleConvertIbanPaytoToAccountField( - wex: WalletExecutionContext, - req: ConvertIbanPaytoToAccountFieldRequest, -): Promise<ConvertIbanPaytoToAccountFieldResponse> { - const payto = Result.unpack(Paytos.fromString(req.paytoUri)); - const iban = payto.normalizedPath; - if (iban.startsWith("HU")) { - let bban = iban.slice(4); - if (bban.endsWith("00000000")) { - bban = bban.slice(0, bban.length - 8); - } - return { - type: "bban", - value: bban, - }; - } else if (wex.ws.devExperimentState.fakeChfBban && iban.startsWith("CH")) { - return { - type: "bban", - value: iban.slice(4), - }; - } - return { - type: "iban", - value: iban, - }; -} +export type NotificationListener = (n: WalletNotification) => void; -export async function handleGetFlightRecords( - wex: WalletExecutionContext, - _req: EmptyObject, -): Promise<TestingGetFlightRecordsResponse> { - return { flightRecords: wex.ws.flightRecords }; -} +type CancelFn = () => void; -export async function handleGetDefaultExchanges( - wex: WalletExecutionContext, - _req: EmptyObject, -): Promise<GetDefaultExchangesResponse> { - const defaultExchanges: GetDefaultExchangesResponse["defaultExchanges"] = []; - const myExchanges = await listExchanges(wex, { - filterByType: "prod", - }); - for (const exch of myExchanges.exchanges) { - switch (exch.exchangeEntryStatus) { - case ExchangeEntryStatus.Ephemeral: - continue; - } - if (exch.currency === "UNKNOWN") { - continue; - } - defaultExchanges.push({ - currency: exch.currency, - currencySpec: exch.currencySpec, - talerUri: TalerUris.stringify({ - type: TalerUriAction.WithdrawExchange, - exchangeBaseUrl: exch.exchangeBaseUrl as HostPortPath, - }), - }); - } - if (wex.ws.devExperimentState.fakeDefaultExchangeDemo) { - defaultExchanges.push({ - talerUri: "taler://withdraw-exchange/exchange.demo.taler.net/", - currency: "KUDOS", - currencySpec: { - name: "Kudos", - common_amounts: ["KUDOS:5", "KUDOS:10", "KUDOS:25", "KUDOS:50"], - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": "ク", - }, - }, - }); - } - return { - defaultExchanges, - }; -} +/** + * Incremented each time we want to re-materialize transactions. + * + * Bumped to 4 for BUG-084: after the status-enum digit fixup + * (fixup20260718StatusEnumDigits) rewrites the raw records, transactionsMeta + * and its byStatus index must be rebuilt from the corrected values. + */ +const MATERIALIZED_TRANSACTIONS_VERSION = 4; -export async function handleTestingCorruptWithdrawalCoinSel( +export async function migrateMaterializedTransactions( wex: WalletExecutionContext, - req: TestingCorruptWithdrawalCoinSelRequest, -): Promise<EmptyObject> { - const txId = parseTransactionIdentifier(req.transactionId); - if (txId?.tag !== TransactionType.Withdrawal) { - throw Error("expected withdrawal transaction ID"); - } +): Promise<void> { await wex.runWalletDbTx(async (tx) => { - const wg = await tx.getWithdrawalGroup(txId.withdrawalGroupId); - if (!wg) { - return; - } - if (wg.denomsSel && (wg.denomsSel.selectedDenoms.length ?? 0) > 0) { - wg.denomsSel.selectedDenoms[0].denomPubHash = encodeCrock( - getRandomBytes(64), - ); - await tx.upsertWithdrawalGroup(wg); - } - }); - return {}; -} - -export async function handleGetDiagnostics( - wex: WalletExecutionContext, - req: EmptyObject, -): Promise<TestingGetDiagnosticsResponse> { - const cnt: Record<string, number> = {}; - const exchangeEntries: TestingGetDiagnosticsResponse["exchangeEntries"] = []; - await wex.db.runAllStoresReadWriteTx({}, async (tx) => { - cnt["coinAvailability"] = await tx.coinAvailability.count(); - cnt["coins"] = await tx.coins.count(); - cnt["denominationFamilies"] = await tx.denominationFamilies.count(); - cnt["denominations"] = await tx.denominations.count(); - cnt["exchangeDetails"] = await tx.exchangeDetails.count(); - cnt["exchangeSignKeys"] = await tx.exchangeSignKeys.count(); - cnt["exchanges"] = await tx.exchanges.count(); - for (const exch of await tx.exchanges.getAll()) { - const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll( - exch.baseUrl, - ); - let numWithdrawableDenoms = 0; - let numCandidateWithdrawableDenoms = 0; - for (let i = 0; i < denoms.length; i++) { - const d = denoms[i]; - if (isWithdrawableDenom(d)) { - numWithdrawableDenoms++; - } - if (isCandidateWithdrawableDenomRec(d)) { - numCandidateWithdrawableDenoms++; - } + const ver = await tx.getConfig( + ConfigRecordKey.MaterializedTransactionsVersion, + ); + if (ver) { + if (ver.value == MATERIALIZED_TRANSACTIONS_VERSION) { + return; + } + if (ver.value > MATERIALIZED_TRANSACTIONS_VERSION) { + logger.error( + "database is newer than code (materializedTransactionsVersion)", + ); + return; } - exchangeEntries.push({ - exchangeBaseUrl: exch.baseUrl, - numDenoms: denoms.length, - numCandidateWithdrawableDenoms, - numWithdrawableDenoms, - }); } - }); - return { - version: 0, - idbObjectStoreCounts: cnt, - exchangeEntries, - }; -} -export async function handleTestingWaitExchangeReady( - wex: WalletExecutionContext, - req: TestingWaitExchangeReadyRequest, -): Promise<EmptyObject> { - await waitReadyExchange(wex, req.exchangeBaseUrl, { - noBail: req.noBail, - forceUpdate: req.forceUpdate, - waitAutoRefresh: req.waitAutoRefresh, - }); - return {}; -} + await rematerializeTransactions(wex, tx); -export async function handleTestingWaitBalance( - wex: WalletExecutionContext, - req: TestingWaitBalanceRequest, -): Promise<EmptyObject> { - await testingWaitBalance(wex, req); - return {}; + await tx.upsertConfig({ + key: ConfigRecordKey.MaterializedTransactionsVersion, + value: MATERIALIZED_TRANSACTIONS_VERSION, + }); + }); } -export async function handleGetPerformanceStats( +export async function getDenomInfo( wex: WalletExecutionContext, - req: GetPerformanceStatsRequest, -): Promise<GetPerformanceStatsResponse> { - return { - stats: PerformanceTable.limit(wex.ws.performanceStats, req.limit), - }; -} - -interface HandlerWithValidator<Tag extends WalletApiOperation> { - codec: Codec<WalletCoreRequestType<Tag>>; - handler: ( - wex: WalletExecutionContext, - req: WalletCoreRequestType<Tag>, - ) => Promise<WalletCoreResponseType<Tag>>; + tx: WalletDbTransaction | LegacyWalletTxHandle, + exchangeBaseUrl: string, + denomPubHash: string, +): Promise<DenominationInfo | undefined> { + const key = `${exchangeBaseUrl}:${denomPubHash}`; + return wex.ws.denomInfoCache.getOrPut(key, async () => { + const d = + "getDenomination" in tx + ? await tx.getDenomination(exchangeBaseUrl, denomPubHash) + : await tx.wtx.getDenomination(exchangeBaseUrl, denomPubHash); + if (d != null) { + return WalletDenomination.toDenomInfo(d); + } else { + return undefined; + } + }); } -const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { - [WalletApiOperation.CancelProgressToken]: { - codec: codecForCancelProgressToken(), - handler: handleCancelProgressToken, - }, - [WalletApiOperation.RetryProgressTokenNow]: { - codec: codecForRetryProgressTokenNowRequest(), - handler: handleRetryProgressTokenNow, - }, - [WalletApiOperation.TestingWaitExchangeReady]: { - codec: codecForTestingWaitExchangeReadyRequest(), - handler: handleTestingWaitExchangeReady, - }, - [WalletApiOperation.TestingWaitBalance]: { - codec: codecForTestingWaitBalanceRequest(), - handler: handleTestingWaitBalance, - }, - [WalletApiOperation.TestingCorruptWithdrawalCoinSel]: { - codec: codecForTestingCorruptWithdrawalCoinSelRequest(), - handler: handleTestingCorruptWithdrawalCoinSel, - }, - [WalletApiOperation.GetDefaultExchanges]: { - codec: codecForEmptyObject(), - handler: handleGetDefaultExchanges, - }, - [WalletApiOperation.TestingGetFlightRecords]: { - codec: codecForEmptyObject(), - handler: handleGetFlightRecords, - }, - [WalletApiOperation.GetDiagnostics]: { - codec: codecForEmptyObject(), - handler: handleGetDiagnostics, - }, - [WalletApiOperation.ConvertIbanAccountFieldToPayto]: { - codec: codecForConvertIbanAccountFieldToPaytoRequest(), - handler: handleConvertIbanAccountFieldToPayto, - }, - [WalletApiOperation.ConvertIbanPaytoToAccountField]: { - codec: codecForConvertIbanPaytoToAccountFieldRequest(), - handler: handleConvertIbanPaytoToAccountField, - }, - [WalletApiOperation.TestingGetPerformanceStats]: { - codec: codecForGetPerformanceStatsRequest(), - handler: handleGetPerformanceStats, - }, - [WalletApiOperation.TestingWaitExchangeState]: { - codec: codecForAny(), - handler: handleTestingWaitExchangeState, - }, - [WalletApiOperation.GetDonauStatements]: { - codec: codecForGetDonauStatementsRequest(), - handler: handleGetDonauStatements, - }, - [WalletApiOperation.GetDonau]: { - codec: codecForEmptyObject(), - handler: handleGetDonau, - }, - [WalletApiOperation.SetDonau]: { - codec: codecForSetDonauRequest(), - handler: handleSetDonau, - }, - [WalletApiOperation.AddContact]: { - codec: codecForAddContactRequest(), - handler: addContact, - }, - [WalletApiOperation.DeleteContact]: { - codec: codecForDeleteContactRequest(), - handler: deleteContact, - }, - [WalletApiOperation.GetContacts]: { - codec: codecForEmptyObject(), - handler: listContacts, - }, - [WalletApiOperation.RegisterAlias]: { - codec: codecForTaldirRegistrationRequest(), - handler: registerAlias, - }, - [WalletApiOperation.CompleteRegisterAlias]: { - codec: codecForTaldirRegistrationCompletionRequest(), - handler: completeAliasRegistration, - }, - [WalletApiOperation.LookupAlias]: { - codec: codecForTaldirLookupRequest(), - handler: lookupAlias, - }, - [WalletApiOperation.InitializeMailbox]: { - codec: codecForMailboxBaseUrl(), - handler: createNewMailbox, - }, - [WalletApiOperation.GetMailbox]: { - codec: codecForMailboxBaseUrl(), - handler: getMailbox, - }, - [WalletApiOperation.SendTalerUriMailboxMessage]: { - codec: codecForSendTalerUriMailboxMessageRequest(), - handler: sendTalerUriMessage, - }, - [WalletApiOperation.AddMailboxMessage]: { - codec: codecForAddMailboxMessageRequest(), - handler: addMailboxMessage, - }, - [WalletApiOperation.DeleteMailboxMessage]: { - codec: codecForDeleteMailboxMessageRequest(), - handler: deleteMailboxMessage, - }, - [WalletApiOperation.GetMailboxMessages]: { - codec: codecForEmptyObject(), - handler: listMailboxMessages, - }, - [WalletApiOperation.RefreshMailbox]: { - codec: codecForMailboxConfiguration(), - handler: refreshMailbox, - }, - [WalletApiOperation.TestingGetDbStats]: { - codec: codecForEmptyObject(), - handler: async (wex) => { - return wex.ws.dbImplementation.getStats(); - }, - }, - [WalletApiOperation.ExportDbToFile]: { - codec: codecForExportDbToFileRequest(), - handler: handleExportDbToFile, - }, - [WalletApiOperation.HintApplicationResumed]: { - codec: codecForEmptyObject(), - handler: handleHintApplicationResumed, - }, - [WalletApiOperation.TestingRunFixup]: { - codec: codecForRunFixupRequest(), - handler: handleTestingRunFixup, - }, - [WalletApiOperation.AbortTransaction]: { - codec: codecForAbortTransaction(), - handler: handleAbortTransaction, - }, - [WalletApiOperation.CreateStoredBackup]: { - codec: codecForEmptyObject(), - handler: handleCreateStoredBackup, - }, - [WalletApiOperation.DeleteStoredBackup]: { - codec: codecForDeleteStoredBackupRequest(), - handler: handleDeleteStoredBackup, - }, - [WalletApiOperation.ListStoredBackups]: { - codec: codecForEmptyObject(), - handler: listStoredBackups, - }, - [WalletApiOperation.CompleteExchangeBaseUrl]: { - codec: codecForCompleteBaseUrlRequest(), - handler: handleCompleteExchangeBaseUrl, - }, - [WalletApiOperation.SetWalletRunConfig]: { - codec: codecForInitRequest(), - handler: handleSetWalletRunConfig, - }, - // Alias for SetWalletRunConfig - [WalletApiOperation.InitWallet]: { - codec: codecForInitRequest(), - handler: handleSetWalletRunConfig, - }, - [WalletApiOperation.RecoverStoredBackup]: { - codec: codecForRecoverStoredBackupRequest(), - handler: handleRecoverStoredBackup, - }, - [WalletApiOperation.WithdrawTestkudos]: { - codec: codecForEmptyObject(), - handler: handleWithdrawTestkudos, - }, - [WalletApiOperation.WithdrawTestBalance]: { - codec: codecForWithdrawTestBalance(), - handler: handleWithdrawTestBalance, - }, - [WalletApiOperation.RunIntegrationTest]: { - codec: codecForIntegrationTestArgs(), - handler: handleRunIntegrationTest, - }, - [WalletApiOperation.RunIntegrationTestV2]: { - codec: codecForIntegrationTestV2Args(), - handler: handleRunIntegrationTestV2, - }, - [WalletApiOperation.ValidateIban]: { - codec: codecForValidateIbanRequest(), - handler: handleValidateIban, - }, - [WalletApiOperation.TestPay]: { - codec: codecForTestPayArgs(), - handler: testPay, - }, - [WalletApiOperation.GetTransactions]: { - codec: codecForTransactionsRequest(), - handler: getTransactions, - }, - [WalletApiOperation.GetTransactionsV2]: { - codec: codecForGetTransactionsV2Request(), - handler: getTransactionsV2, - }, - [WalletApiOperation.GetTransactionById]: { - codec: codecForTransactionByIdRequest(), - handler: getTransactionById, - }, - [WalletApiOperation.AddExchange]: { - codec: codecForAddExchangeRequest(), - handler: handleAddExchange, - }, - [WalletApiOperation.TestingPing]: { - codec: codecForEmptyObject(), - handler: async () => ({}), - }, - [WalletApiOperation.UpdateExchangeEntry]: { - codec: codecForUpdateExchangeEntryRequest(), - handler: handleUpdateExchangeEntry, - }, - [WalletApiOperation.TestingGetDenomStats]: { - codec: codecForTestingGetDenomStatsRequest(), - handler: handleTestingGetDenomStats, - }, - [WalletApiOperation.ListExchanges]: { - codec: codecForListExchangesRequest(), - handler: listExchanges, - }, - [WalletApiOperation.GetExchangeEntryByUrl]: { - codec: codecForGetExchangeEntryByUrlRequest(), - handler: lookupExchangeByUri, - }, - [WalletApiOperation.GetExchangeDetailedInfo]: { - codec: codecForGetExchangeEntryByUrlRequest(), - handler: (wex, req) => getExchangeDetailedInfo(wex, req.exchangeBaseUrl), - }, - [WalletApiOperation.ListBankAccounts]: { - codec: codecForListBankAccounts(), - handler: handleListBankAccounts, - }, - [WalletApiOperation.GetBankAccountById]: { - codec: codecForGetBankAccountByIdRequest(), - handler: handleGetBankAccountById, - }, - [WalletApiOperation.AddBankAccount]: { - codec: codecForAddBankAccountRequest(), - handler: handleAddBankAccount, - }, - [WalletApiOperation.ForgetBankAccount]: { - codec: codecForForgetBankAccount(), - handler: handleForgetBankAccount, - }, - [WalletApiOperation.GetWithdrawalDetailsForUri]: { - codec: codecForGetWithdrawalDetailsForUri(), - handler: (wex, req) => - getWithdrawalDetailsForUri(wex, req.talerWithdrawUri), - }, - [WalletApiOperation.TestingGetReserveHistory]: { - codec: codecForTestingGetReserveHistoryRequest(), - handler: handleTestingGetReserveHistory, - }, - [WalletApiOperation.AcceptManualWithdrawal]: { - codec: codecForAcceptManualWithdrawalRequest(), - handler: handleAcceptManualWithdrawal, - }, - [WalletApiOperation.GetWithdrawalDetailsForAmount]: { - codec: codecForGetWithdrawalDetailsForAmountRequest(), - handler: getWithdrawalDetailsForAmount, - }, - [WalletApiOperation.GetBalances]: { - codec: codecForEmptyObject(), - handler: getBalances, - }, - [WalletApiOperation.GetBalanceDetail]: { - codec: codecForGetBalanceDetailRequest(), - handler: getBalanceDetail, - }, - [WalletApiOperation.SetExchangeTosAccepted]: { - codec: codecForAcceptExchangeTosRequest(), - handler: async (wex, req) => { - await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl); - return {}; - }, - }, - [WalletApiOperation.SetExchangeTosForgotten]: { - codec: codecForAcceptExchangeTosRequest(), - handler: async (wex, req) => { - await forgetExchangeTermsOfService(wex, req.exchangeBaseUrl); - return {}; - }, - }, - [WalletApiOperation.AcceptBankIntegratedWithdrawal]: { - codec: codecForAcceptBankIntegratedWithdrawalRequest(), - handler: handleAcceptBankIntegratedWithdrawal, - }, - [WalletApiOperation.ConfirmWithdrawal]: { - codec: codecForConfirmWithdrawalRequestRequest(), - handler: confirmWithdrawal, - }, - [WalletApiOperation.PrepareBankIntegratedWithdrawal]: { - codec: codecForPrepareBankIntegratedWithdrawalRequest(), - handler: prepareBankIntegratedWithdrawal, - }, - [WalletApiOperation.GetExchangeTos]: { - codec: codecForGetExchangeTosRequest(), - handler: handleGetExchangeTos, - }, - [WalletApiOperation.SharePayment]: { - codec: codecForSharePaymentRequest(), - handler: handleSharePayment, - }, - [WalletApiOperation.PrepareWithdrawExchange]: { - codec: codecForPrepareWithdrawExchangeRequest(), - handler: handlePrepareWithdrawExchange, - }, - [WalletApiOperation.CheckPayForTemplate]: { - codec: codecForCheckPayTemplateRequest(), - handler: checkPayForTemplate, - }, - [WalletApiOperation.PreparePayForUri]: { - codec: codecForPreparePayRequest(), - handler: (wex, req) => preparePayForUri(wex, req.talerPayUri), - }, - [WalletApiOperation.PreparePayForTemplate]: { - codec: codecForPreparePayTemplateRequest(), - handler: preparePayForTemplate, - }, - [WalletApiOperation.PreparePayForUriV2]: { - codec: codecForPreparePayRequest(), - handler: (wex, req) => preparePayForUriV2(wex, req.talerPayUri), - }, - [WalletApiOperation.PreparePayForTemplateV2]: { - codec: codecForPreparePayTemplateRequest(), - handler: preparePayForTemplateV2, - }, - [WalletApiOperation.GetQrCodesForPayto]: { - codec: codecForGetQrCodesForPaytoRequest(), - handler: handleGetQrCodesForPayto, - }, - [WalletApiOperation.GetChoicesForPayment]: { - codec: codecForGetChoicesForPaymentRequest(), - handler: handleGetChoicesForPayment, - }, - [WalletApiOperation.ConfirmPay]: { - codec: codecForConfirmPayRequest(), - handler: handleConfirmPay, - }, - [WalletApiOperation.ListDiscounts]: { - codec: codecForListDiscountsRequest(), - handler: handleListDiscounts, - }, - [WalletApiOperation.DeleteDiscount]: { - codec: codecForDeleteDiscountRequest(), - handler: handleDeleteDiscount, - }, - [WalletApiOperation.ListSubscriptions]: { - codec: codecForListSubscriptionsRequest(), - handler: handleListSubscriptions, - }, - [WalletApiOperation.DeleteSubscription]: { - codec: codecForDeleteSubscriptionRequest(), - handler: handleDeleteSubscription, - }, - [WalletApiOperation.SuspendTransaction]: { - codec: codecForSuspendTransaction(), - handler: handleSuspendTransaction, - }, - [WalletApiOperation.GetActiveTasks]: { - codec: codecForEmptyObject(), - handler: handleGetActiveTasks, - }, - [WalletApiOperation.FailTransaction]: { - codec: codecForFailTransactionRequest(), - handler: handleFailTransaction, - }, - [WalletApiOperation.ResumeTransaction]: { - codec: codecForResumeTransaction(), - handler: async (wex, req) => { - await resumeTransaction(wex, req.transactionId); - return {}; - }, - }, - [WalletApiOperation.DumpCoins]: { - codec: codecForEmptyObject(), - handler: dumpCoins, - }, - [WalletApiOperation.SetCoinSuspended]: { - codec: codecForSetCoinSuspendedRequest(), - handler: async (wex, req) => { - await setCoinSuspended(wex, req.coinPub, req.suspended); - return {}; - }, - }, - [WalletApiOperation.TestingGetSampleTransactions]: { - codec: codecForEmptyObject(), - handler: handleTestingGetSampleTransactions, - }, - [WalletApiOperation.StartRefundQueryForUri]: { - codec: codecForPrepareRefundRequest(), - handler: (wex, req) => startRefundQueryForUri(wex, req.talerRefundUri), - }, - [WalletApiOperation.StartRefundQuery]: { - codec: codecForStartRefundQueryRequest(), - handler: handleStartRefundQuery, - }, - [WalletApiOperation.TestingWaitTransactionState]: { - codec: codecForAny(), - handler: async (wex, req) => { - await waitTransactionState(wex, req); - return {}; - }, - }, - [WalletApiOperation.GetCurrencySpecification]: { - codec: codecForGetCurrencyInfoRequest(), - handler: handleGetCurrencySpecification, - }, - [WalletApiOperation.HintNetworkAvailability]: { - codec: codecForHintNetworkAvailabilityRequest(), - handler: handleHintNetworkAvailability, - }, - [WalletApiOperation.ConvertDepositAmount]: { - codec: codecForConvertAmountRequest, - handler: convertDepositAmount, - }, - [WalletApiOperation.GetMaxDepositAmount]: { - codec: codecForGetMaxDepositAmountRequest(), - handler: getMaxDepositAmount, - }, - [WalletApiOperation.GetMaxPeerPushDebitAmount]: { - codec: codecForGetMaxPeerPushDebitAmountRequest(), - handler: getMaxPeerPushDebitAmount, - }, - [WalletApiOperation.CheckDeposit]: { - codec: codecForCheckDepositRequest(), - handler: checkDepositGroup, - }, - [WalletApiOperation.CreateDepositGroup]: { - codec: codecForCreateDepositGroupRequest(), - handler: createDepositGroup, - }, - [WalletApiOperation.DeleteTransaction]: { - codec: codecForDeleteTransactionRequest(), - handler: async (wex, req) => { - await deleteTransaction(wex, req.transactionId); - return {}; - }, - }, - [WalletApiOperation.RetryTransaction]: { - codec: codecForRetryTransactionRequest(), - handler: async (wex, req) => { - await retryTransaction(wex, req.transactionId); - return {}; - }, - }, - [WalletApiOperation.TestCrypto]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - return await wex.cryptoApi.hashString({ str: "hello world" }); - }, - }, - [WalletApiOperation.ClearDb]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - await clearDatabase(wex.db.idbHandle()); - wex.ws.flightRecords = []; - wex.ws.clearAllCaches(); - await wex.taskScheduler.reload(); - return {}; - }, - }, - [WalletApiOperation.Recycle]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - throw Error("not implemented"); - }, - }, - [WalletApiOperation.ExportDb]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - const dbDump = await exportDb(wex.ws.idbFactory); - return dbDump; - }, - }, - [WalletApiOperation.GetDepositWireTypes]: { - codec: codecForGetDepositWireTypesRequest(), - handler: handleGetDepositWireTypes, - }, - [WalletApiOperation.GetDepositWireTypesForCurrency]: { - codec: codecForGetDepositWireTypesForCurrencyRequest(), - handler: handleGetDepositWireTypesForCurrency, - }, - [WalletApiOperation.ListGlobalCurrencyExchanges]: { - codec: codecForEmptyObject(), - handler: handleListGlobalCurrencyExchanges, - }, - [WalletApiOperation.ListGlobalCurrencyAuditors]: { - codec: codecForEmptyObject(), - handler: handleListGlobalCurrencyAuditors, - }, - [WalletApiOperation.AddGlobalCurrencyExchange]: { - codec: codecForAddGlobalCurrencyExchangeRequest(), - handler: handleAddGlobalCurrencyExchange, - }, - [WalletApiOperation.RemoveGlobalCurrencyExchange]: { - codec: codecForRemoveGlobalCurrencyExchangeRequest(), - handler: handleRemoveGlobalCurrencyExchange, - }, - [WalletApiOperation.AddGlobalCurrencyAuditor]: { - codec: codecForAddGlobalCurrencyAuditorRequest(), - handler: handleAddGlobalCurrencyAuditor, - }, - [WalletApiOperation.TestingWaitTasksDone]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - await waitTasksDone(wex); - return {}; - }, - }, - [WalletApiOperation.TestingResetAllRetries]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - await retryAll(wex); - return {}; - }, - }, - [WalletApiOperation.RemoveGlobalCurrencyAuditor]: { - codec: codecForRemoveGlobalCurrencyAuditorRequest(), - handler: handleRemoveGlobalCurrencyAuditor, - }, - [WalletApiOperation.ImportDb]: { - codec: codecForImportDbRequest(), - handler: handleImportDb, - }, - [WalletApiOperation.ImportDbFromFile]: { - codec: codecForImportDbFromFileRequest(), - handler: handleImportDbFromFile, - }, - [WalletApiOperation.CheckPeerPushDebit]: { - codec: codecForCheckPeerPushDebitRequest(), - handler: checkPeerPushDebit, - }, - [WalletApiOperation.CheckPeerPushDebitV2]: { - codec: codecForCheckPeerPushDebitRequest(), - handler: checkPeerPushDebitV2, - }, - [WalletApiOperation.InitiatePeerPushDebit]: { - codec: codecForInitiatePeerPushDebitRequest(), - handler: initiatePeerPushDebit, - }, - [WalletApiOperation.PreparePeerPushCredit]: { - codec: codecForPreparePeerPushCreditRequest(), - handler: preparePeerPushCredit, - }, - [WalletApiOperation.ConfirmPeerPushCredit]: { - codec: codecForConfirmPeerPushPaymentRequest(), - handler: confirmPeerPushCredit, - }, - [WalletApiOperation.CheckPeerPullCredit]: { - codec: codecForPreparePeerPullPaymentRequest(), - handler: checkPeerPullCredit, - }, - [WalletApiOperation.InitiatePeerPullCredit]: { - codec: codecForInitiatePeerPullPaymentRequest(), - handler: initiatePeerPullPayment, - }, - [WalletApiOperation.PreparePeerPullDebit]: { - codec: codecForCheckPeerPullPaymentRequest(), - handler: preparePeerPullDebit, - }, - [WalletApiOperation.ConfirmPeerPullDebit]: { - codec: codecForAcceptPeerPullPaymentRequest(), - handler: confirmPeerPullDebit, - }, - [WalletApiOperation.ApplyDevExperiment]: { - codec: codecForApplyDevExperiment(), - handler: async (wex, req) => { - await applyDevExperiment(wex, req.devExperimentUri); - return {}; - }, - }, - [WalletApiOperation.Shutdown]: { - codec: codecForEmptyObject(), - handler: handleShutdown, - }, - [WalletApiOperation.GetVersion]: { - codec: codecForEmptyObject(), - handler: handleGetVersion, - }, - [WalletApiOperation.TestingWaitTransactionsFinal]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - await waitUntilAllTransactionsFinal(wex); - return {}; - }, - }, - [WalletApiOperation.TestingWaitRefreshesFinal]: { - codec: codecForEmptyObject(), - handler: async (wex, req) => { - await waitUntilRefreshesDone(wex); - return {}; - }, - }, - [WalletApiOperation.TestingSetTimetravel]: { - codec: codecForTestingSetTimetravelRequest(), - handler: async (wex, req) => { - await handleTestingSetTimetravel(wex, req); - return {}; - }, - }, - [WalletApiOperation.DeleteExchange]: { - codec: codecForDeleteExchangeRequest(), - handler: handleDeleteExchange, - }, - [WalletApiOperation.GetExchangeResources]: { - codec: codecForGetExchangeResourcesRequest(), - handler: async (wex, req) => { - return await getExchangeResources(wex, req.exchangeBaseUrl); - }, - }, - [WalletApiOperation.CanonicalizeBaseUrl]: { - codec: codecForCanonicalizeBaseUrlRequest(), - handler: handleCanonicalizeBaseUrl, - }, - [WalletApiOperation.ForceRefresh]: { - codec: codecForForceRefreshRequest(), - handler: async (wex, req) => { - await forceRefresh(wex, req); - return {}; - }, - }, - [WalletApiOperation.ListAssociatedRefreshes]: { - codec: codecForAny(), - handler: async (wex, req) => { - throw Error("not implemented"); - }, - }, - [WalletApiOperation.GetBankingChoicesForPayto]: { - codec: codecForGetBankingChoicesForPaytoRequest(), - handler: handleGetBankingChoicesForPayto, - }, - [WalletApiOperation.StartExchangeWalletKyc]: { - codec: codecForStartExchangeWalletKycRequest(), - handler: handleStartExchangeWalletKyc, - }, - [WalletApiOperation.TestingWaitExchangeWalletKyc]: { - codec: codecForTestingWaitWalletKycRequest(), - handler: handleTestingWaitExchangeWalletKyc, - }, - [WalletApiOperation.TestingPlanMigrateExchangeBaseUrl]: { - codec: codecForTestingPlanMigrateExchangeBaseUrlRequest(), - handler: handleTestingPlanMigrateExchangeBaseUrl, - }, -}; - /** - * Implementation of the "wallet-core" API. + * Get an API client from an internal wallet state object. */ -async function dispatchRequestInternal( - wex: WalletExecutionContext, - operation: WalletApiOperation, - payload: unknown, -): Promise<WalletCoreResponseType<typeof operation>> { - if (!wex.ws.initCalled && operation !== WalletApiOperation.InitWallet) { - throw Error( - `wallet must be initialized before running operation ${operation}`, - ); - } - - const h: HandlerWithValidator<any> = handlers[operation]; - - if (!h) { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN, - { - operation, - }, - "unknown operation", - ); - } - - const req = h.codec.decode(payload); - return await h.handler(wex, req); +let id = 0; +async function getClientFromWalletState( + ws: InternalWalletState, +): Promise<WalletCoreApiClient> { + const client: WalletCoreApiClient = { + async call(op, payload): Promise<any> { + id = (id + 1) % (Number.MAX_SAFE_INTEGER - 100); + const res = await dispatchWalletCoreApiRequest( + ws, + op, + String(id), + payload, + ); + switch (res.type) { + case "error": + throw TalerError.fromUncheckedDetail(res.error); + case "response": + return res.result; + } + }, + callForResult: async function <Op extends keyof WalletOperations>( + op: Op, + payload: WalletCoreRequestType<Op>, + ): Promise<Result<WalletCoreResponseType<Op>, WalletCoreErrorType<Op>>> { + id = (id + 1) % (Number.MAX_SAFE_INTEGER - 100); + const res = await dispatchWalletCoreApiRequest( + ws, + op, + String(id), + payload, + ); + switch (res.type) { + case "error": { + if (op in walletApiExpectedErrors) { + const errs = ( + walletApiExpectedErrors as { + [x: string]: readonly TalerErrorCode[]; + } + )[op]; + if (errs.includes(res.error.code)) { + return Result.errorWithDetail( + res.error.code as WalletCoreErrorType<Op>, + res.error, + ); + } + } + throw TalerError.fromUncheckedDetail(res.error); + } + case "response": + return Result.of(res.result as WalletCoreResponseType<Op>); + default: + assertUnreachable(res); + } + }, + }; + return client; } export function getObservedWalletExecutionContext( @@ -3149,7 +548,9 @@ async function dispatchWalletCoreApiRequest( } } -function applyRunConfigDefaults(wcp?: PartialWalletRunConfig): WalletRunConfig { +export function applyRunConfigDefaults( + wcp?: PartialWalletRunConfig, +): WalletRunConfig { if (wcp?.features?.allowHttp != null) { logger.warn(`allowHttp flag not supported anymore`); } @@ -3240,65 +641,6 @@ export class Wallet { } } -export class Cache<T> { - private map: Map<string, [AbsoluteTime, T]> = new Map(); - - constructor( - private maxCapacity: number, - private cacheDuration: Duration, - ) {} - - get(key: string): T | undefined { - const r = this.map.get(key); - if (!r) { - return undefined; - } - - if (AbsoluteTime.isExpired(r[0])) { - this.map.delete(key); - return undefined; - } - - return r[1]; - } - - async getOrPut(key: string, lambda: () => Promise<T>): Promise<T>; - async getOrPut( - key: string, - lambda: () => Promise<T | undefined>, - ): Promise<T | undefined>; - async getOrPut( - key: string, - lambda: () => Promise<T | undefined>, - ): Promise<T | undefined> { - const cached = this.get(key); - if (cached != null) { - return cached; - } else { - const computed = await lambda(); - if (computed != null) { - this.put(key, computed); - } - return computed; - } - } - - clear(): void { - this.map.clear(); - } - - put(key: string, value: T): void { - if (this.map.size > this.maxCapacity) { - this.map.clear(); - } - const expiry = AbsoluteTime.addDuration( - AbsoluteTime.now(), - this.cacheDuration, - ); - this.map.set(key, [expiry, value]); - } -} - /** * Implementation of triggers for the wallet DB. */