taler-typescript-core

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

commit 6cc957a8a7e3596874799bf7db1e1c81dcb75480
parent a1574b823ac02956a268ffcf201c7b8ec40e674a
Author: Florian Dold <florian@dold.me>
Date:   Wed, 15 Jul 2026 20:22:28 +0200

split db.ts into common/indexeddb

Diffstat:
Mpackages/taler-wallet-core/src/balance.ts | 10++++++----
Mpackages/taler-wallet-core/src/coinSelection.ts | 2+-
Mpackages/taler-wallet-core/src/common.ts | 12+++++++-----
Mpackages/taler-wallet-core/src/contacts.ts | 2+-
Apackages/taler-wallet-core/src/db-common.ts | 665+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-core/src/db-indexeddb.ts | 3782+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-wallet-core/src/db.ts | 4403-------------------------------------------------------------------------------
Mpackages/taler-wallet-core/src/dbtx.ts | 2+-
Mpackages/taler-wallet-core/src/denomSelection.ts | 4++--
Mpackages/taler-wallet-core/src/denominations.ts | 4++--
Mpackages/taler-wallet-core/src/deposits.ts | 23++++++++++++++---------
Mpackages/taler-wallet-core/src/dev-experiments.ts | 10++++++----
Mpackages/taler-wallet-core/src/donau.ts | 5++---
Mpackages/taler-wallet-core/src/exchanges.ts | 22++++++++++++----------
Mpackages/taler-wallet-core/src/index.ts | 5+++--
Mpackages/taler-wallet-core/src/instructedAmountConversion.ts | 3++-
Mpackages/taler-wallet-core/src/kyc.ts | 2+-
Mpackages/taler-wallet-core/src/pay-merchant.ts | 20+++++++++++---------
Mpackages/taler-wallet-core/src/pay-peer-common.ts | 2+-
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 12+++++++-----
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 8+++++---
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 12+++++++-----
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 8+++++---
Mpackages/taler-wallet-core/src/preset-exchanges.ts | 2+-
Mpackages/taler-wallet-core/src/recoup.ts | 10++++++----
Mpackages/taler-wallet-core/src/refresh.ts | 12+++++++-----
Mpackages/taler-wallet-core/src/shepherd.ts | 8+++++---
Mpackages/taler-wallet-core/src/tokenFamilies.ts | 2+-
Mpackages/taler-wallet-core/src/tokenSelection.ts | 7++-----
Mpackages/taler-wallet-core/src/transactions.ts | 4+++-
Mpackages/taler-wallet-core/src/wallet.ts | 10++++++----
Mpackages/taler-wallet-core/src/withdraw.test.ts | 6+++---
Mpackages/taler-wallet-core/src/withdraw.ts | 18++++++++++--------
33 files changed, 4587 insertions(+), 4510 deletions(-)

diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts @@ -81,7 +81,6 @@ import { import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js"; import { DepositOperationStatus, - DonationSummaryRecord, ExchangeEntryDbRecordStatus, OPERATION_STATUS_NONFINAL_FIRST, OPERATION_STATUS_NONFINAL_LAST, @@ -90,12 +89,15 @@ import { PeerPushCreditStatus, PeerPushDebitStatus, PurchaseStatus, - RefreshGroupRecord, RefreshOperationStatus, - WalletIndexedDbTransaction, WithdrawalGroupStatus, +} from "./db-common.js"; +import { + DonationSummaryRecord, + RefreshGroupRecord, + WalletIndexedDbTransaction, WithdrawalRecordType, -} from "./db.js"; +} from "./db-indexeddb.js"; import { checkExchangeInScopeTx, getExchangeDetailsInTx, diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts @@ -61,7 +61,7 @@ import { PaymentBalanceDetails, } from "./balance.js"; import { getAutoRefreshExecuteThreshold } from "./common.js"; -import { DenominationRecord, WalletIndexedDbTransaction } from "./db.js"; +import { DenominationRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; import { checkExchangeInScopeTx, ExchangeDetails, diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts @@ -58,12 +58,15 @@ import { } from "@gnu-taler/taler-util"; import { HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; import { - CoinHistoryRecord, - CoinRecord, DbPreciseTimestamp, - DepositGroupRecord, ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, + timestampPreciseToDb, +} from "./db-common.js"; +import { + CoinHistoryRecord, + CoinRecord, + DepositGroupRecord, ExchangeEntryRecord, PeerPullCreditRecord, PeerPullPaymentIncomingRecord, @@ -74,8 +77,7 @@ import { RefreshGroupRecord, WalletIndexedDbTransaction, WithdrawalGroupRecord, - timestampPreciseToDb, -} from "./db.js"; +} from "./db-indexeddb.js"; import { ReadyExchangeSummary } from "./exchanges.js"; import { createRefreshGroup } from "./refresh.js"; import { BalanceEffect, applyNotifyTransition } from "./transactions.js"; diff --git a/packages/taler-wallet-core/src/contacts.ts b/packages/taler-wallet-core/src/contacts.ts @@ -30,7 +30,7 @@ import { Logger, NotificationType, } from "@gnu-taler/taler-util"; -import { ContactRecord, WalletIndexedDbTransaction } from "./db.js"; +import { ContactRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; import { WalletExecutionContext } from "./wallet.js"; const logger = new Logger("contacts.ts"); diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts @@ -0,0 +1,665 @@ +/* + 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/> + */ + +import { + AbsoluteTime, + TalerPreciseTimestamp, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; + +declare const symDbProtocolTimestamp: unique symbol; + +declare const symDbPreciseTimestamp: unique symbol; + +/** + * Timestamp, stored as microseconds. + * + * Always rounded to a full second. + */ +export type DbProtocolTimestamp = number & { [symDbProtocolTimestamp]: true }; + +/** + * Timestamp, stored as microseconds. + */ +export type DbPreciseTimestamp = number & { [symDbPreciseTimestamp]: true }; + +const DB_TIMESTAMP_FOREVER = Number.MAX_SAFE_INTEGER; + +export function timestampPreciseFromDb( + dbTs: DbPreciseTimestamp, +): TalerPreciseTimestamp { + return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000)); +} + +export function timestampOptionalPreciseFromDb( + dbTs: DbPreciseTimestamp | undefined, +): TalerPreciseTimestamp | undefined { + if (!dbTs) { + return undefined; + } + return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000)); +} + +export function timestampPreciseToDb( + stamp: TalerPreciseTimestamp, +): DbPreciseTimestamp { + if (stamp.t_s === "never") { + return DB_TIMESTAMP_FOREVER as DbPreciseTimestamp; + } else { + let tUs = stamp.t_s * 1000000; + if (stamp.off_us) { + tUs += stamp.off_us; + } + return tUs as DbPreciseTimestamp; + } +} + +export function timestampProtocolToDb( + stamp: TalerProtocolTimestamp, +): DbProtocolTimestamp { + if (stamp.t_s === "never") { + return DB_TIMESTAMP_FOREVER as DbProtocolTimestamp; + } else { + let tUs = stamp.t_s * 1000000; + return tUs as DbProtocolTimestamp; + } +} + +export function timestampProtocolFromDb( + stamp: DbProtocolTimestamp, +): TalerProtocolTimestamp { + return TalerProtocolTimestamp.fromSeconds(Math.floor(stamp / 1000000)); +} + +export function timestampAbsoluteFromDb( + stamp: DbProtocolTimestamp | DbPreciseTimestamp, +): AbsoluteTime { + if (stamp >= DB_TIMESTAMP_FOREVER) { + return AbsoluteTime.never(); + } + return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000)); +} + +export function timestampOptionalAbsoluteFromDb( + stamp: DbProtocolTimestamp | DbPreciseTimestamp | undefined, +): AbsoluteTime | undefined { + if (stamp == null) { + return undefined; + } + if (stamp >= DB_TIMESTAMP_FOREVER) { + return AbsoluteTime.never(); + } + return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000)); +} + +/** + * Status of a denomination. + */ +export enum DenominationVerificationStatus { + /** + * Verification was delayed (pending). + */ + Unverified = 0x0100_0000, + + /** + * Verified as valid. + */ + VerifiedGood = 0x0500_0000, + + /** + * Verified as invalid. + */ + VerifiedBad = 0x0501_0000, +} + +/** + * Format of the operation status code: 0x0abc_nnnn + + * a=1: active + * 0x0100_nnnn: pending + * 0x0101_nnnn: dialog + * 0x0102_nnnn: (reserved) + * 0x0103_nnnn: aborting + * 0x0110_nnnn: suspended + * 0x0113_nnnn: suspended-aborting + * a=2: finalizing + * 0x0200_nnnn: finalizing + * 0x0210_nnnn: suspended-finalizing + * a=5: final + * 0x0500_nnnn: done + * 0x0501_nnnn: failed + * 0x0502_nnnn: expired + * 0x0503_nnnn: aborted + * + * nnnn=0000 should always be the most generic minor state for the major state + */ + +/** + * First possible operation status in the active range (inclusive). + */ +export const OPERATION_STATUS_NONFINAL_FIRST = 0x0100_0000; + +/** + * LAST possible operation status in the active range (inclusive). + */ +export const OPERATION_STATUS_NONFINAL_LAST = 0x0210_ffff; + +export const OPERATION_STATUS_DIALOG_FIRST = 0x0101_0000; +export const OPERATION_STATUS_DIALOG_LAST = 0x0101_ffff; + +export const OPERATION_STATUS_DONE_FIRST = 0x0500_0000; +export const OPERATION_STATUS_DONE_LAST = 0x0500_ffff; + +/** + * Status of a withdrawal. + */ +export enum WithdrawalGroupStatus { + /** + * Reserve must be registered with the bank. + */ + PendingRegisteringBank = 0x0100_0001, + SuspendedRegisteringBank = 0x0110_0001, + + /** + * We've registered reserve's information with the bank + * and are now waiting for the user to confirm the withdraw + * with the bank (typically 2nd factor auth). + */ + PendingWaitConfirmBank = 0x0100_0002, + SuspendedWaitConfirmBank = 0x0110_0002, + + /** + * Querying reserve status with the exchange. + */ + PendingQueryingStatus = 0x0100_0003, + SuspendedQueryingStatus = 0x0110_0003, + + /** + * Ready for withdrawal. + */ + PendingReady = 0x0100_0004, + SuspendedReady = 0x0110_0004, + + /** + * Redenominate the withdrawal + * after the exchange entry is ready again. + */ + PendingRedenominate = 0x0100_0008, + SuspendedRedenominate = 0x0110_0008, + + /** + * Exchange wants KYC info from the user. + */ + PendingKyc = 0x0100_0005, + SuspendedKyc = 0x0110_005, + + /** + * Exchange wants KYC info from the user. + * KYC link is ready. + */ + PendingBalanceKyc = 0x0100_0006, + SuspendedBalanceKyc = 0x0110_006, + + /** + * Exchange wants KYC info from the user. + * + * KYC link is not ready yet, the KYC process is still initializing. + */ + PendingBalanceKycInit = 0x0100_0007, + SuspendedBalanceKycInit = 0x0110_007, + + /** + * Proposed to the user, has can choose to accept/refuse. + */ + DialogProposed = 0x0101_0000, + + /** + * We are telling the bank that we don't want to complete + * the withdrawal! + */ + AbortingBank = 0x0103_0001, + SuspendedAbortingBank = 0x0113_0001, + + /** + * The corresponding withdraw record has been created. + * No further processing is done, unless explicitly requested + * by the user. + */ + Done = 0x0500_0000, + + /** + * The bank aborted the withdrawal. + */ + FailedBankAborted = 0x0501_0001, + + FailedAbortingBank = 0x0501_0002, + + /** + * Aborted in a state where we were supposed to + * talk to the exchange. Money might have been + * wired or not. + */ + AbortedExchange = 0x0503_0001, + + AbortedBank = 0x0503_0002, + + /** + * User didn't refused the withdrawal. + */ + AbortedUserRefused = 0x0503_0003, + + /** + * Another wallet confirmed the withdrawal + * (by POSTing the reserve pub to the bank) + * before we had the chance. + * + * In this situation, we'll let the other wallet continue + * and give up ourselves. + */ + AbortedOtherWallet = 0x0503_0004, +} + +export enum ExchangeEntryDbRecordStatus { + Preset = 1, + Ephemeral = 2, + Used = 3, +} + +// FIXME: Use status ranges for this as well? +export enum ExchangeEntryDbUpdateStatus { + Initial = 1, + InitialUpdate = 2, + Suspended = 3, + UnavailableUpdate = 4, + // Reserved 5 for backwards compatibility. + Ready = 6, + ReadyUpdate = 7, + OutdatedUpdate = 8, +} + +export enum PlanchetStatus { + Pending = 0x0100_0000, + KycRequired = 0x0100_0001, + WithdrawalDone = 0x0500_000, + AbortedReplaced = 0x0503_0001, +} + +export enum RefreshCoinStatus { + Pending = 0x0100_0000, + + /** + * Re-try the melt with a new target denomination. + */ + PendingRedenominate = 0x0100_0001, + + Finished = 0x0500_0000, + + /** + * The refresh for this coin has been frozen, because of a permanent error. + * More info in lastErrorPerCoin. + */ + Failed = 0x0501_000, +} + +export enum RefreshOperationStatus { + Pending = 0x0100_0000, + /** + * Entire output coin selection was bad, re-select + * and potentially revive finished coins with zero output. + */ + PendingRedenominate = 0x0100_0001, + Suspended = 0x0110_0000, + SuspendedRedenominate = 0x0110_0001, + + Finished = 0x0500_000, + Failed = 0x0501_000, +} + +/** + * Status of a single element of a deposit group. + */ +export enum DepositElementStatus { + DepositPending = 0x0100_0000, + /** + * Accepted, but tracking. + */ + Tracking = 0x0100_0001, + KycRequired = 0x0100_0002, + Wired = 0x0500_0000, + RefundSuccess = 0x0503_0000, + RefundFailed = 0x0501_0000, + RefundNotFound = 0x0501_0001, +} + +export enum PurchaseStatus { + /** + * Not downloaded yet. + */ + PendingDownloadingProposal = 0x0100_0000, + SuspendedDownloadingProposal = 0x0110_0000, + + /** + * The user has accepted the proposal. + */ + PendingPaying = 0x0100_0001, + SuspendedPaying = 0x0110_0001, + + /** + * Currently in the process of aborting with a refund. + */ + AbortingWithRefund = 0x0103_0000, + SuspendedAbortingWithRefund = 0x0113_0000, + + /** + * Paying a second time, likely with different session ID + */ + PendingPayingReplay = 0x0100_0002, + SuspendedPayingReplay = 0x0110_0002, + + /** + * Query for refunds (until query succeeds). + */ + PendingQueryingRefund = 0x0100_0003, + SuspendedQueryingRefund = 0x0110_0003, + + /** + * Query for refund (until auto-refund deadline is reached). + * + * Legacy state for compatibility. + */ + PendingQueryingAutoRefund = 0x0100_0004, + SuspendedQueryingAutoRefund = 0x0110_0004, + + FinalizingQueryingAutoRefund = 0x0200_0001, + SuspendedFinalizingQueryingAutoRefund = 0x0210_0001, + + PendingAcceptRefund = 0x0100_0005, + SuspendedPendingAcceptRefund = 0x0110_0005, + + /** + * Proposal downloaded, but the user needs to accept/reject it. + */ + DialogProposed = 0x0101_0000, + + /** + * Proposal shared to other wallet or read from other wallet + * the user needs to accept/reject it. + */ + DialogShared = 0x0101_0001, + + /** + * Generic failure, check error code. + */ + Failed = 0x0501_0000, + + /** + * Tried to abort, but aborting failed or was cancelled. + */ + FailedAbort = 0x0501_0001, + + FailedPaidByOther = 0x0501_0002, + + /** + * Downloading or processing the proposal has failed permanently. + */ + FailedClaim = 0x0501_0003, + + /** + * Payment was successful. + */ + Done = 0x0500_0000, + + /** + * Downloaded proposal was detected as a re-purchase. + */ + DoneRepurchaseDetected = 0x0500_0001, + + Expired = 0x0502_0000, + + /** + * The user has rejected the proposal. + */ + AbortedProposalRefused = 0x0503_0000, + + AbortedRefunded = 0x0503_0001, + + AbortedOrderDeleted = 0x0503_0002, + + /** + * The payment has been aborted. + */ + AbortedIncompletePayment = 0x0503_0003, +} + +export enum ConfigRecordKey { + WalletBackupState = "walletBackupState", + CurrencyDefaultsApplied = "currencyDefaultsApplied", + // Only for testing, do not use! + TestLoopTx = "testTxLoop", + LastInitInfo = "lastInitInfo", + MaterializedTransactionsVersion = "materializedTransactionsVersion", + DonauConfig = "donauConfig", +} + +export enum RecoupOperationStatus { + Pending = 0x0100_0000, + Suspended = 0x0110_0000, + + Finished = 0x0500_000, + Failed = 0x0501_000, +} +export enum DepositOperationStatus { + PendingDeposit = 0x0100_0000, + SuspendedDeposit = 0x0110_0000, + + // Legacy states, we we now show + // the tracking state as a finalizing state. + LegacyPendingTrack = 0x0100_0001, + LegacySuspendedTrack = 0x0110_0001, + + PendingAggregateKyc = 0x0100_0002, + SuspendedAggregateKyc = 0x0110_0002, + + PendingDepositKyc = 0x0100_0003, + SuspendedDepositKyc = 0x0110_0003, + + PendingDepositKycAuth = 0x0100_0005, + SuspendedDepositKycAuth = 0x0110_0005, + + Aborting = 0x0103_0000, + SuspendedAborting = 0x0113_0000, + + FinalizingTrack = 0x0200_0001, + SuspendedFinalizingTrack = 0x0210_0001, + + Finished = 0x0500_0000, + + FailedDeposit = 0x0501_0000, + + FailedTrack = 0x0501_0001, + + AbortedDeposit = 0x0503_0000, +} + +export enum PeerPushDebitStatus { + /** + * Initiated, but no purse created yet. + */ + PendingCreatePurse = 0x0100_0000 /* ACTIVE_START */, + PendingReady = 0x0100_0001, + AbortingDeletePurse = 0x0103_0000, + + SuspendedCreatePurse = 0x0110_0000, + SuspendedReady = 0x0110_0001, + SuspendedAbortingDeletePurse = 0x0113_0000, + + Done = 0x0500_0000, + Aborted = 0x0503_0000, + Failed = 0x0501_0000, + Expired = 0x0502_0000, + + // Legacy / reserved: + // SuspendedAbortingRefreshDeleted = 0x0113_0001, + // SuspendedAbortingRefreshExpired = 0x0113_0002, + // AbortingRefreshDeleted = 0x0103_0001, + // AbortingRefreshExpired = 0x0103_0002, +} + +export enum PeerPullPaymentCreditStatus { + /** + * Typically the initial state of the peer-pull-credit transaction, + * purse will be created. + */ + PendingCreatePurse = 0x0100_0000, + SuspendedCreatePurse = 0x0110_0000, + + /** + * Purse created, waiting for the other party to accept the + * invoice and deposit money into it. + */ + PendingReady = 0x0100_0001, + SuspendedReady = 0x0110_0001, + + PendingMergeKycRequired = 0x0100_0002, + SuspendedMergeKycRequired = 0x0110_0002, + + PendingWithdrawing = 0x0100_0003, + SuspendedWithdrawing = 0x0110_0003, + + PendingBalanceKycRequired = 0x0100_0004, + SuspendedBalanceKycRequired = 0x0110_0004, + + PendingBalanceKycInit = 0x0100_0005, + SuspendedBalanceKycInit = 0x0110_0005, + + AbortingDeletePurse = 0x0103_0000, + SuspendedAbortingDeletePurse = 0x0113_0000, + + Done = 0x0500_0000, + Failed = 0x0501_0000, + Expired = 0x0502_0000, + Aborted = 0x0503_0000, +} + +export enum PeerPushCreditStatus { + PendingMerge = 0x0100_0000, + SuspendedMerge = 0x0110_0000, + + PendingMergeKycRequired = 0x0100_0001, + SuspendedMergeKycRequired = 0x0110_0001, + + /** + * Merge was successful and withdrawal group has been created, now + * everything is in the hand of the withdrawal group. + */ + PendingWithdrawing = 0x0100_0002, + SuspendedWithdrawing = 0x0110_0002, + + PendingBalanceKycRequired = 0x0100_0003, + SuspendedBalanceKycRequired = 0x0110_0003, + + PendingBalanceKycInit = 0x0100_0004, + SuspendedBalanceKycInit = 0x0110_0004, + + DialogProposed = 0x0101_0000, + + Done = 0x0500_0000, + Aborted = 0x0503_0000, + Failed = 0x0501_0000, + Expired = 0x0502_0000, +} + +export enum PeerPullDebitRecordStatus { + PendingDeposit = 0x0100_0001, + AbortingRefresh = 0x0103_0001, + + SuspendedDeposit = 0x0110_0001, + SuspendedAbortingRefresh = 0x0113_0001, + + DialogProposed = 0x0101_0001, + + Done = 0x0500_0000, + Expired = 0x0502_0000, + Aborted = 0x0503_0000, + Failed = 0x0501_0000, +} + +export enum ReserveRecordStatus { + // Need to call the "/kyc-wallet" endpoint + PendingLegiInit = 0x0100_0001, + SuspendedLegiInit = 0x0110_0001, + // Need to wait for user to pass legitimization + PendingLegi = 0x0100_0002, + SuspendedLegi = 0x0110_0002, + + /** + * Done with KYC. + */ + Done = 0x0500_0000, +} + +export enum RefundGroupStatus { + Pending = 0x0100_0000, + Done = 0x0500_0000, + Failed = 0x0501_0000, + Aborted = 0x0503_0000, + Expired = 0x0502_0000, +} + +export enum RefundItemStatus { + /** + * Intermittent error that the merchant is + * reporting from the exchange. + * + * We'll try again! + */ + Pending = 0x0100_0000, + /** + * Refund was obtained successfully. + */ + Done = 0x0500_0000, + /** + * Permanent error reported by the exchange + * for the refund. + */ + Failed = 0x0501_0000, +} + +export enum DenomLossStatus { + /** + * Done indicates that the loss happened. + */ + Done = 0x0500_0000, + + /** + * Aborted in the sense that the loss was reversed. + */ + Aborted = 0x0503_0001, +} + +export enum DonationReceiptStatus { + /** + * Done indicates that the receipt + * has been successfully submitted. + */ + DoneSubmitted = 0x0500_0000, + + /** + * Pending indicates that the + * receipt still needs to be submitted. + */ + Pending = 0x0100_0000, +} diff --git a/packages/taler-wallet-core/src/db-indexeddb.ts b/packages/taler-wallet-core/src/db-indexeddb.ts @@ -0,0 +1,3782 @@ +/* + This file is part of GNU Taler + (C) 2021-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/> + */ + +/** + * Imports. + */ +import { + Event, + GlobalIDB, + IDBDatabase, + IDBFactory, + IDBKeyRange, + IDBObjectStore, + IDBRequest, + IDBTransaction, + structuredEncapsulate, + structuredRevive, +} from "@gnu-taler/idb-bridge"; +import { + AccountLimit, + AgeCommitmentProof, + AmountString, + Amounts, + BlindedUniqueDonationIdentifier, + CancellationToken, + Codec, + CoinEnvelope, + CoinPublicKeyString, + CoinRefreshRequest, + CoinStatus, + CurrencySpecification, + DenomLossEventType, + DenomSelectionState, + DenominationInfo, + DenominationPubKey, + DonationReceiptSignature, + EddsaPublicKeyString, + EddsaSignatureString, + ExchangeAuditor, + ExchangeGlobalFees, + ExchangeRefundRequest, + HashCodeString, + Logger, + MailboxConfiguration, + MailboxMessageRecord, + MerchantContractTokenDetails, + MerchantContractTokenKind, + RefreshReason, + ScopeInfo, + SignedTokenEnvelope, + TalerErrorCode, + TalerErrorDetail, + TalerProtocolDuration, + TalerProtocolTimestamp, + TokenEnvelope, + TokenIssuePublicKey, + TokenUseSig, + TransactionIdStr, + TransferOptionRaw, + UnblindedDenominationSignature, + WireInfo, + WithdrawalExchangeAccountDetails, + ZeroLimitedOperation, + canonicalJson, + checkDbInvariant, + codecForAny, + encodeCrock, + hash, + j2s, + stringToBytes, +} from "@gnu-taler/taler-util"; +import { DbRetryInfo, TaskIdentifiers } from "./common.js"; +import { + ConfigRecordKey, + DbPreciseTimestamp, + DbProtocolTimestamp, + DenomLossStatus, + DenominationVerificationStatus, + DepositElementStatus, + DepositOperationStatus, + DonationReceiptStatus, + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, + PeerPullDebitRecordStatus, + PeerPullPaymentCreditStatus, + PeerPushCreditStatus, + PeerPushDebitStatus, + PlanchetStatus, + PurchaseStatus, + RecoupOperationStatus, + RefreshCoinStatus, + RefreshOperationStatus, + RefundGroupStatus, + RefundItemStatus, + ReserveRecordStatus, + WithdrawalGroupStatus, + timestampProtocolFromDb, +} from "./db-common.js"; +import { + DbAccess, + DbAccessImpl, + DbReadWriteTransaction, + IndexDescriptor, + StoreDescriptor, + StoreNames, + StoreWithIndexes, + describeContents, + describeIndex, + describeStore, + describeStoreV2, + openDatabase, +} from "./query.js"; + +/** + * This file contains the database schema of the Taler wallet together + * with some helper functions. + * + * Some design considerations: + * - By convention, each object store must have a corresponding "<Name>Record" + * interface defined for it. + * - For records that represent operations, there should be exactly + * one top-level enum field that indicates the status of the operation. + * This field should be present even if redundant, because the field + * will have an index. + * - Amounts are stored as strings, except when they are needed for + * indexing. + * - Every record that has a corresponding transaction item must have + * an index for a mandatory timestamp field. + * - Optional fields should be avoided, use "T | undefined" instead. + * - Do all records have some obvious, indexed field that can + * be used for range queries? + * + * @author Florian Dold <dold@taler.net> + */ + +/** + FIXMEs: + - Contract terms can be quite large. We currently tend to read the + full contract terms from the DB quite often. + Instead, we should probably extract what we need into a separate object + store. + - More object stores should have an "id" primary key, + as this makes referencing less expensive. + - Coin selections should probably go into a separate object store. + - Some records should be split up into an extra "details" record + that we don't always need to iterate over. + */ + +/** + * Name of the Taler database. This is effectively the major + * version of the DB schema. Whenever it changes, custom import logic + * for all previous versions must be written, which should be + * avoided. + */ +export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v10"; + +/** + * Name of the metadata database. This database is used + * to track major migrations of the main Taler database. + * + * (Minor migrations are handled via upgrade transactions.) + */ +export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; + +/** + * Name of the "stored backups" database. + * Stored backups are created before manually importing a backup. + * We use IndexedDB for this purpose, since we don't have file system + * access on some platforms. + */ +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = + "taler-wallet-stored-backups"; + +/** + * Name of the "meta config" database. + */ +export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; + +/** + * Current database minor version, should be incremented + * each time we do minor schema changes on the database. + * A change is considered minor when fields are added in a + * backwards-compatible way or object stores and indices + * are added. + */ +export const WALLET_DB_MINOR_VERSION = 29; + +export interface DenomFamilyParams { + exchangeBaseUrl: string; + exchangeMasterPub: string; + value: AmountString; + feeWithdraw: AmountString; + feeDeposit: AmountString; + feeRefresh: AmountString; + feeRefund: AmountString; +} + +/** + * Extra info about a withdrawal that is used + * with a bank-integrated withdrawal. + */ +export interface ReserveBankInfo { + talerWithdrawUri: string; + + /** + * URL that the user can be redirected to, and allows + * them to confirm (or abort) the bank-integrated withdrawal. + */ + confirmUrl: string | undefined; + + /** + * Exchange payto URI that the bank will use to fund the reserve. + */ + exchangePaytoUri?: string; + + /** + * Time when the information about this reserve was posted to the bank. + * + * Only applies if bankWithdrawStatusUrl is defined. + * + * Set to undefined if that hasn't happened yet. + */ + timestampReserveInfoPosted: DbPreciseTimestamp | undefined; + + /** + * Time when the reserve was confirmed by the bank. + * + * Set to undefined if not confirmed yet. + */ + timestampBankConfirmed: DbPreciseTimestamp | undefined; + + wireTypes: string[] | undefined; + + currency: string | undefined; + + externalConfirmation?: boolean; + + senderWire?: string; +} + +export interface DenomFees { + /** + * Fee for withdrawing. + */ + feeWithdraw: AmountString; + + /** + * Fee for depositing. + */ + feeDeposit: AmountString; + + /** + * Fee for refreshing. + */ + feeRefresh: AmountString; + + /** + * Fee for refunding. + */ + feeRefund: AmountString; +} + +export interface DenominationFamilyRecord { + denominationFamilySerial?: number; + familyParams: DenomFamilyParams; + + // Reserved legacy fields: + // * familyParamsHash +} + +/** + * Denomination record as stored in the wallet's database. + */ +export interface DenominationRecord { + /** + * Currency of the denomination. + * + * Stored separately as we have an index on it. + */ + currency: string; + + value: AmountString; + + /** + * The denomination public key. + */ + denomPub: DenominationPubKey; + + /** + * Hash of the denomination public key. + * Stored in the database for faster lookups. + */ + denomPubHash: string; + + fees: DenomFees; + + denominationFamilySerial: number; + + /** + * Validity start date of the denomination. + */ + stampStart: DbProtocolTimestamp; + + /** + * Date after which the currency can't be withdrawn anymore. + */ + stampExpireWithdraw: DbProtocolTimestamp; + + /** + * Date after the denomination officially doesn't exist anymore. + */ + stampExpireLegal: DbProtocolTimestamp; + + /** + * Data after which coins of this denomination can't be deposited anymore. + */ + stampExpireDeposit: DbProtocolTimestamp; + + /** + * Signature by the exchange's master key over the denomination + * information. + */ + masterSig: string; + + /** + * Did we verify the signature on the denomination? + */ + verificationStatus: DenominationVerificationStatus; + + /** + * Was this denomination still offered by the exchange the last time + * we checked? + * Only false when the exchange redacts a previously published denomination. + */ + isOffered: boolean; + + /** + * Did the exchange revoke the denomination? + * When this field is set to true in the database, the same transaction + * should also mark all affected coins as revoked. + */ + isRevoked: boolean; + + /** + * If set to true, the exchange announced that the private key for this + * denomination is lost. Thus it can't be used to sign new coins + * during withdrawal/refresh/..., but the coins can still be spent. + */ + isLost?: boolean; + + /** + * Base URL of the exchange. + */ + exchangeBaseUrl: string; + + /** + * Master public key of the exchange that made the signature + * on the denomination. + */ + exchangeMasterPub: string; +} + +export namespace DenominationRecord { + export function toDenomInfo(d: DenominationRecord): DenominationInfo { + return { + denomPub: d.denomPub, + exchangeMasterPub: d.exchangeMasterPub, + denomPubHash: d.denomPubHash, + feeDeposit: Amounts.stringify(d.fees.feeDeposit), + feeRefresh: Amounts.stringify(d.fees.feeRefresh), + feeRefund: Amounts.stringify(d.fees.feeRefund), + feeWithdraw: Amounts.stringify(d.fees.feeWithdraw), + stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit), + stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal), + stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw), + stampStart: timestampProtocolFromDb(d.stampStart), + value: Amounts.stringify(d.value), + exchangeBaseUrl: d.exchangeBaseUrl, + isLost: d.isLost ?? false, + masterSig: d.masterSig, + isOffered: d.isOffered, + }; + } +} + +export interface ExchangeSignkeysRecord { + stampStart: DbProtocolTimestamp; + stampExpire: DbProtocolTimestamp; + stampEnd: DbProtocolTimestamp; + signkeyPub: EddsaPublicKeyString; + masterSig: EddsaSignatureString; + + /** + * Exchange details that thiis signkeys record belongs to. + */ + exchangeDetailsRowId: number; +} + +/** + * Exchange details for a particular + * (exchangeBaseUrl, masterPublicKey, currency) tuple. + */ +export interface ExchangeDetailsRecord { + rowId?: number; + + /** + * Master public key of the exchange. + */ + masterPublicKey: string; + + exchangeBaseUrl: string; + + /** + * Currency that the exchange offers. + */ + currency: string; + + /** + * Auditors (partially) auditing the exchange. + */ + auditors: ExchangeAuditor[]; + + /** + * Last observed protocol version. + */ + protocolVersionRange: string; + + tinyAmount: AmountString; + + reserveClosingDelay: TalerProtocolDuration; + + shoppingUrl?: string; + + /** + * Fees for exchange services + */ + globalFees: ExchangeGlobalFees[]; + + wireInfo: WireInfo; + + /** + * Age restrictions supported by the exchange (bitmask). + */ + ageMask?: number; + + walletBalanceLimits?: AmountString[]; + + hardLimits?: AccountLimit[]; + + zeroLimits?: ZeroLimitedOperation[]; + + /** + * Instructs wallets to use certain bank-specific + * language (for buttons) and/or other UI/UX customization + * for compliance with the rules of that bank. + */ + bankComplianceLanguage: string | undefined; + + defaultPeerPushExpiration: TalerProtocolDuration | undefined; +} + +export interface ExchangeDetailsPointer { + masterPublicKey: string; + + currency: string; + + /** + * Timestamp when the (masterPublicKey, currency) pointer + * has been updated. + */ + updateClock: DbPreciseTimestamp; +} + +/** + * Exchange record as stored in the wallet's database. + */ +export interface ExchangeEntryRecord { + /** + * Base url of the exchange. + */ + baseUrl: string; + + /** + * Currency hint for a preset exchange, relevant + * when we didn't contact a preset exchange yet. + */ + presetCurrencyHint?: string; + + /** + * Currency spec for a preset exchange, relevant + * when we didn't contact a preset exchange yet. + */ + presetCurrencySpec?: CurrencySpecification; + + /** + * Type of the exchange, if it was a preset entry. + */ + presetType?: string; + + /** + * When did we confirm the last withdrawal from this exchange? + * + * Used mostly in the UI to suggest exchanges. + */ + lastWithdrawal?: DbPreciseTimestamp; + + /** + * Pointer to the current exchange details. + * + * Should usually not change. Only changes when the + * exchange advertises a different master public key and/or + * currency. + * + * We could use a rowID here, but having the currency in the + * details pointer lets us do fewer DB queries + */ + detailsPointer: ExchangeDetailsPointer | undefined; + + entryStatus: ExchangeEntryDbRecordStatus; + + updateStatus: ExchangeEntryDbUpdateStatus; + + unavailableReason?: TalerErrorDetail; + + /** + * If set to true, the next update to the exchange + * status will request /keys with no-cache headers set. + */ + cachebreakNextUpdate?: boolean; + + /** + * Etag of the current ToS of the exchange. + */ + tosCurrentEtag: string | undefined; + + tosAcceptedEtag: string | undefined; + + tosAcceptedTimestamp: DbPreciseTimestamp | undefined; + + /** + * Last time when the exchange /keys info was updated + * successfully. + */ + lastUpdate: DbPreciseTimestamp | undefined; + + /** + * Next scheduled update for the exchange. + */ + nextUpdateStamp: DbPreciseTimestamp; + + lastKeysEtag: string | undefined; + + /** + * Next time that we should check if coins need to be refreshed. + * + * Updated whenever the exchange's denominations are updated or when + * the refresh check has been done. + */ + nextRefreshCheckStamp: DbPreciseTimestamp; + + /** + * Public key of the reserve that we're currently using for + * receiving P2P payments. + */ + currentMergeReserveRowId?: number; + + /** + * Current account private key. The corresponding public + * key is used as the merchant public key in deposits. + * + * When unset or reset, we use a heuristic to find an + * account priv/pub that likely already has KYC auth. + */ + currentAccountPriv?: string; + + /** + * @see currentAccountPriv + */ + currentAccountPub?: string; + + /** + * Defaults to false. + */ + peerPaymentsDisabled?: boolean; + + /** + * Are direct deposits using this exchange disabled? + * Defaults to false. + */ + directDepositDisabled?: boolean; + + /** + * Defaults to false. + */ + noFees?: boolean; +} + +/** + * A coin that isn't yet signed by an exchange. + */ +export interface PlanchetRecord { + /** + * Public key of the coin. + */ + coinPub: string; + + /** + * Private key of the coin. + */ + coinPriv: string; + + /** + * Withdrawal group that this planchet belongs to + * (or the empty string). + */ + withdrawalGroupId: string; + + /** + * Index within the withdrawal group (or -1). + */ + coinIdx: number; + + planchetStatus: PlanchetStatus; + + lastError: TalerErrorDetail | undefined; + + denomPubHash: string; + + blindingKey: string; + + withdrawSig: string; + + coinEv: CoinEnvelope; + + coinEvHash: string; + + ageCommitmentProof?: AgeCommitmentProof; +} + +export enum CoinSourceType { + Withdraw = "withdraw", + Refresh = "refresh", + Reward = "reward", +} + +export interface WithdrawCoinSource { + type: CoinSourceType.Withdraw; + + /** + * Can be the empty string for orphaned coins. + */ + withdrawalGroupId: string; + + /** + * Index of the coin in the withdrawal session. + */ + coinIndex: number; + + /** + * Reserve public key for the reserve we got this coin from. + */ + reservePub: string; +} + +export interface RefreshCoinSource { + type: CoinSourceType.Refresh; + refreshGroupId: string; + oldCoinPub: string; +} + +export interface RewardCoinSource { + type: CoinSourceType.Reward; + walletRewardId: string; + coinIndex: number; +} + +export type CoinSource = + | WithdrawCoinSource + | RefreshCoinSource + | RewardCoinSource; + +/** + * CoinRecord as stored in the "coins" data store + * of the wallet database. + */ +export interface CoinRecord { + /** + * Where did the coin come from? Used for recouping coins. + */ + coinSource: CoinSource; + + /** + * Source transaction ID of the coin. + * + * Used to make the coin visible after the transaction + * has entered a final state. + */ + sourceTransactionId?: string; + + /** + * Public key of the coin. + */ + coinPub: string; + + /** + * Private key to authorize operations on the coin. + */ + coinPriv: string; + + /** + * Hash of the public key that signs the coin. + */ + denomPubHash: string; + + /** + * Unblinded signature by the exchange. + */ + denomSig: UnblindedDenominationSignature; + + /** + * Base URL that identifies the exchange from which we got the + * coin. + */ + exchangeBaseUrl: string; + + /** + * Blinding key used when withdrawing the coin. + * Potentionally used again during payback. + */ + blindingKey: string; + + /** + * Hash of the coin envelope. + * + * Stored here for indexing purposes, so that when looking at a + * reserve history, we can quickly find the coin for a withdrawal transaction. + */ + coinEvHash: string; + + /** + * Status of the coin. + */ + status: CoinStatus; + + /** + * Non-zero for visible. + * + * A coin is visible when it is fresh and the + * source transaction is in a final state. + */ + visible?: number; + + /** + * Maximum age of purchases that can be made with this coin. + * + * (Used for indexing, redundant with {@link ageCommitmentProof}). + */ + maxAge: number; + + ageCommitmentProof: AgeCommitmentProof | undefined; +} + +/** + * Object to be hashed for use as a grouping key for token listings, such that + * any change in token family details results in a separate list item. + */ +export interface TokenFamilyInfo { + /** + * Identifier for the token family consisting of + * unreserved characters according to RFC 3986. + */ + slug: string; + + /** + * Human-readable name for the token family. + */ + name: string; + + /** + * Human-readable description for the token family. + */ + description: string; + + /** + * Optional map from IETF BCP 47 language tags to localized descriptions. + */ + descriptionI18n: any | undefined; + + /** + * Additional meta data, such as the trusted_domains + * or expected_domains. Depends on the kind. + */ + extraData: MerchantContractTokenDetails; + + /** + * Token issue public key used by merchant to verify tokens. + */ + tokenIssuePub: TokenIssuePublicKey; +} + +/** + * TokenFamilyRecord as stored in the "tokenFamilies" + * data store of the wallet database. + */ +export interface TokenRecord extends TokenFamilyInfo { + /** + * Source purchase of the token. + */ + purchaseId: string; + + /** + * Transaction where token is being used. + */ + transactionId?: string; + + /** + * Index of token in choices array. + */ + choiceIndex?: number; + + /** + * Index of token in outputs array. + */ + outputIndex?: number; + + /** + * For token outputs with a count>1, this stores + * the index of this token within the same output + * index. + * + * If missing, assumed to be 0. + */ + repeatIndex?: number; + + /** + * URL of the merchant issuing the token. + */ + merchantBaseUrl: string; + + /** + * Kind of the token. + */ + kind: MerchantContractTokenKind; + + /** + * Hash of token issue public key. + */ + tokenIssuePubHash: string; + + /** + * Hash of {@link TokenFamilyInfo} object. + */ + tokenFamilyHash?: string; + + /** + * Start time of the token family's validity period. + */ + validAfter: DbProtocolTimestamp; + + /** + * End time of the token family's validity period. + */ + validBefore: DbProtocolTimestamp; + + /** + * Unblinded token issue signature made by the merchant. + */ + tokenIssueSig: UnblindedDenominationSignature; + + /** + * Token use public key used to confirm usage of tokens. + */ + tokenUsePub: string; + + /** + * Token use private key used to verify usage of tokens. + */ + tokenUsePriv: string; + + /** + * Signature on token use request. + */ + tokenUseSig?: TokenUseSig; + + /** + * Envelope of the token. + */ + tokenEv: TokenEnvelope; + + /** + * Hash of the envelope. + */ + tokenEvHash: string; + + /** + * Blinding secret for token. + */ + blindingKey: string; +} + +/** + * Slate, a blank slice of rock cut for use as a writing surface, + * also the database representation of a token before being + * signed by the merchant, as stored in the `slates' data store. + */ +export type SlateRecord = Omit<TokenRecord, "tokenIssueSig">; + +export namespace TokenRecord { + export function hashInfo(r: TokenRecord | SlateRecord): string { + const info: TokenFamilyInfo = { + slug: r.slug, + name: r.name, + description: r.description, + descriptionI18n: r.descriptionI18n, + extraData: r.extraData, + tokenIssuePub: r.tokenIssuePub, + }; + return encodeCrock(hash(stringToBytes(canonicalJson(info) + "\0"))); + } +} + +/** + * History item for a coin. + * + * DB-specific format, + */ +export type DbWalletCoinHistoryItem = + | { + type: "withdraw"; + transactionId: TransactionIdStr; + } + | { + type: "spend"; + transactionId: TransactionIdStr; + amount: AmountString; + } + | { + type: "refresh"; + transactionId: TransactionIdStr; + amount: AmountString; + } + | { + type: "recoup"; + transactionId: TransactionIdStr; + amount: AmountString; + } + | { + type: "refund"; + transactionId: TransactionIdStr; + amount: AmountString; + }; + +/** + * History event for a coin from the wallet's perspective. + * + * The history might reference transactions that were already deleted from the wallet. + */ +export interface CoinHistoryRecord { + coinPub: string; + /** + * History items for the coin. + * + * We store this as an array in the object store, as the coin history + * is pretty much always very small. + */ + history: DbWalletCoinHistoryItem[]; +} + +export interface RefreshGroupPerExchangeInfo { + /** + * (Expected) output once the refresh group succeeded. + */ + outputEffective: AmountString; +} + +/** + * Group of refresh operations. The refreshed coins do not + * have to belong to the same exchange, but must have the same + * currency. + */ +export interface RefreshGroupRecord { + operationStatus: RefreshOperationStatus; + + /** + * Unique, randomly generated identifier for this group of + * refresh operations. + */ + refreshGroupId: string; + + /** + * Currency of this refresh group. + */ + currency: string; + + /** + * Reason why this refresh group has been created. + */ + reason: RefreshReason; + + originatingTransactionId?: string; + + oldCoinPubs: string[]; + + inputPerCoin: AmountString[]; + + expectedOutputPerCoin: AmountString[]; + + infoPerExchange?: Record<string, RefreshGroupPerExchangeInfo>; + + /** + * Flag for each coin whether refreshing finished. + * If a coin can't be refreshed (remaining value too small), + * it will be marked as finished, but no refresh session will + * be created. + */ + statusPerCoin: RefreshCoinStatus[]; + + /** + * Refund requests that might still be necessary + * before the refresh can work. + */ + refundRequests: { [n: number]: ExchangeRefundRequest }; + + timestampCreated: DbPreciseTimestamp; + + failReason?: TalerErrorDetail; + + /** + * Timestamp when the refresh session finished. + */ + timestampFinished: DbPreciseTimestamp | undefined; +} + +/** + * Ongoing refresh + */ +export interface RefreshSessionRecord { + refreshGroupId: string; + + /** + * Index of the coin in the refresh group. + */ + coinIndex: number; + + /** + * If this field is set, it's a V2 refresh session. + */ + sessionPublicSeed?: string; + + /** + * Sum of the value of denominations we want + * to withdraw in this session, without fees. + */ + amountRefreshOutput: AmountString; + + /** + * Hashed denominations of the newly requested coins. + */ + newDenoms: { + denomPubHash: string; + count: number; + }[]; + + /** + * The no-reveal-index after we've done the melting. + */ + norevealIndex?: number; + + /** + * Last error response from the exchange. + * + * FIXME: We don't store the last HTTP status yet. + */ + lastError?: TalerErrorDetail; + + // Reserved legacy fields: + // * sessionSecretSeed: string + // (legacy v1 refresh) +} + +export enum RefundReason { + /** + * Normal refund given by the merchant. + */ + NormalRefund = "normal-refund", + /** + * Refund from an aborted payment. + */ + AbortRefund = "abort-pay-refund", +} + +/** + * Partial information about the downloaded proposal. + * Only contains data that is relevant for indexing on the + * "purchases" object stores. + */ +export interface ProposalDownloadInfo { + contractTermsHash: string; + fulfillmentUrl?: string; + currency: string; + contractTermsMerchantSig: string; +} + +export interface DbCoinSelection { + coinPubs: string[]; + coinContributions: AmountString[]; +} + +export interface DbTokenSelection { + tokenPubs: string[]; +} + +export interface PurchasePayInfo { + /** + * Undefined if payment is blocked by a pending refund. + */ + payCoinSelection?: DbCoinSelection; + /** + * Undefined if payment is blocked by a pending refund. + */ + payCoinSelectionUid?: string; + + payTokenSelection?: DbTokenSelection; + + /** + * Token signatures from merchant. + */ + slateTokenSigs?: SignedTokenEnvelope[]; + + /** + * Whether token selection should be forced + * e.g. when merchant URL is not in `expected_domains' + */ + payTokenForcedSel?: boolean; + + totalPayCost: AmountString; +} + +/** + * Record that stores status information about one purchase, starting from when + * the customer accepts a proposal. Includes refund status if applicable. + * + * Key: {@link proposalId} + * Operation status: {@link purchaseStatus} + */ +export interface PurchaseRecord { + /** + * Proposal ID for this purchase. Uniquely identifies the + * purchase and the proposal. + * Assigned by the wallet. + */ + proposalId: string; + + /** + * Order ID, assigned by the merchant. + */ + orderId: string; + + merchantBaseUrl: string; + + /** + * Claim token used when downloading the contract terms. + */ + claimToken: string | undefined; + + /** + * Session ID we got when downloading the contract. + */ + downloadSessionId: string | undefined; + + /** + * If this purchase is a repurchase, this field identifies the original purchase. + */ + repurchaseProposalId: string | undefined; + + purchaseStatus: PurchaseStatus; + + /** + * Refresh group ID of the refresh transaction that + * has been created to abort the payment. + */ + abortRefreshGroupId?: string; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; + + /** + * Private key for the nonce. + */ + noncePriv: string; + + /** + * Public key for the nonce. + */ + noncePub: string; + + /** + * Index of selected choice in the choices array. + */ + choiceIndex?: number | undefined; + + /** + * Secret seed used to derive slates. + * Stored since slates are created lazily. + */ + secretSeed: string | undefined; + + /** + * Downloaded and parsed proposal data. + */ + download: ProposalDownloadInfo | undefined; + + payInfo: PurchasePayInfo | undefined; + + /** + * Exchanges involved in this purchase. + * Used as a multiEntry index to find all purchases for + * an exchange. + */ + exchanges?: string[]; + + /** + * Pending removals from pay coin selection. + * + * Used when a the pay coin selection needs to be changed + * because a coin became known as double-spent or invalid, + * but a new coin selection can't immediately be done, as + * there is not enough balance (e.g. when waiting for a refresh). + */ + pendingRemovedCoinPubs?: string[]; + + /** + * Timestamp of the first time that sending a payment to the merchant + * for this purchase was successful. + */ + timestampFirstSuccessfulPay: DbPreciseTimestamp | undefined; + + merchantPaySig: string | undefined; + + posConfirmation: string | undefined; + + donauOutputIndex?: number; + donauBaseUrl?: string; + donauAmount?: AmountString; + donauTaxIdHash?: string; + donauTaxIdSalt?: string; + donauTaxId?: string; + donauYear?: number; + + /** + * This purchase was shared with another wallet + * that is now supposed to finish the payment. + */ + shared: boolean; + + /** + * This purchase was created by reading + * a payment share or the wallet + * the nonce public by a payment share + * + * Defaults to false. + */ + createdFromShared?: boolean; + + /** + * When was the purchase record created? + */ + timestamp: DbPreciseTimestamp; + + /** + * When was the purchase made? + * Refers to the time that the user accepted. + */ + timestampAccept: DbPreciseTimestamp | undefined; + + /** + * When was the last refund made? + * Set to 0 if no refund was made on the purchase. + */ + timestampLastRefundStatus: DbPreciseTimestamp | undefined; + + /** + * Timestamp when the wallet noticed that the transaction expired. + * May be later than the pay deadline. + */ + timestampExpired?: DbPreciseTimestamp; + + /** + * Last session signature that we submitted to /pay (if any). + */ + lastSessionId: string | undefined; + + /** + * Continue querying the refund status until this deadline has expired. + */ + autoRefundDeadline: DbProtocolTimestamp | undefined; + + /** + * How much merchant has refund to be taken but the wallet + * did not picked up yet + */ + refundAmountAwaiting: AmountString | undefined; +} + +export interface DonauConfig { + donauBaseUrl: string; + donauTaxId: string; + /** Tax ID hash, salted with donauSalt */ + donauTaxIdHash: string; + /** 32 byte salt, base32crockford encoded */ + donauSalt: string; +} + +/** + * Configuration key/value entries to configure + * the wallet. + */ +export type ConfigRecord = + | { + key: ConfigRecordKey.WalletBackupState; + value: WalletBackupConfState; + } + | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean | number } + | { key: ConfigRecordKey.TestLoopTx; value: number } + | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp } + | { key: ConfigRecordKey.MaterializedTransactionsVersion; value: number } + | { key: ConfigRecordKey.DonauConfig; value: DonauConfig }; + +export interface WalletBackupConfState { + deviceId: string; + walletRootPub: string; + walletRootPriv: string; + + /** + * Last hash of the canonicalized plain-text backup. + */ + lastBackupPlainHash?: string; + + /** + * Timestamp stored in the last backup. + */ + lastBackupTimestamp?: DbPreciseTimestamp; + + /** + * Last time we tried to do a backup. + */ + lastBackupCheckTimestamp?: DbPreciseTimestamp; + lastBackupNonce?: string; +} + +// FIXME: Should these be numeric codes? +export const enum WithdrawalRecordType { + BankManual = "bank-manual", + BankIntegrated = "bank-integrated", + PeerPullCredit = "peer-pull-credit", + PeerPushCredit = "peer-push-credit", + Recoup = "recoup", +} + +export interface WgInfoBankIntegrated { + withdrawalType: WithdrawalRecordType.BankIntegrated; + + /** + * Extra state for when this is a withdrawal involving + * a Taler-integrated bank. + */ + bankInfo: ReserveBankInfo; + + /** + * Info about withdrawal accounts, possibly including currency conversion. + */ + exchangeCreditAccounts?: WithdrawalExchangeAccountDetails[]; +} + +export interface WgInfoBankManual { + withdrawalType: WithdrawalRecordType.BankManual; + + /** + * Info about withdrawal accounts, possibly including currency conversion. + */ + exchangeCreditAccounts?: WithdrawalExchangeAccountDetails[]; +} + +export interface WgInfoBankPeerPull { + withdrawalType: WithdrawalRecordType.PeerPullCredit; + + // FIXME: include a transaction ID here? + + /** + * Needed to quickly construct the taler:// URI for the counterparty + * without a join. + */ + contractPriv: string; +} + +export interface WgInfoBankPeerPush { + withdrawalType: WithdrawalRecordType.PeerPushCredit; + + // FIXME: include a transaction ID here? +} + +export interface WgInfoBankRecoup { + withdrawalType: WithdrawalRecordType.Recoup; +} + +export type WgInfo = + | WgInfoBankIntegrated + | WgInfoBankManual + | WgInfoBankPeerPull + | WgInfoBankPeerPush + | WgInfoBankRecoup; + +export type KycUserType = "individual" | "business"; + +/** + * Group of withdrawal operations that need to be executed. + * (Either for a normal withdrawal or from a reward.) + * + * The withdrawal group record is only created after we know + * the coin selection we want to withdraw. + */ +export interface WithdrawalGroupRecord { + /** + * Unique identifier for the withdrawal group. + */ + withdrawalGroupId: string; + + wgInfo: WgInfo; + + /** + * If set to true, the account used during withdrawal is treated as an + * account that does not belong to the user. It won't be shown in + * the list of know bank accounts. + * + * Defaults to false. + */ + isForeignAccount?: boolean; + + kycPaytoHash?: string; + + kycAccessToken?: string; + + kycLastCheckStatus?: number | undefined; + kycLastCheckCode?: number | undefined; + kycLastRuleGen?: number | undefined; + kycLastAmlReview?: boolean | undefined; + kycLastDeny?: DbPreciseTimestamp | undefined; + + /** + * Delay to wait until the next withdrawal attempt. + * + * @deprecated by https://bugs.gnunet.org/view.php?id=9694 + */ + kycWithdrawalDelay?: TalerProtocolDuration; + + /** + * Secret seed used to derive planchets. + * Stored since planchets are created lazily. + */ + secretSeed: string; + + /** + * Public key of the reserve that we're withdrawing from. + */ + reservePub: string; + + /** + * The reserve private key. + * + * FIXME: Already in the reserves object store, redundant! + */ + reservePriv: string; + + /** + * The exchange base URL that we're withdrawing from. + * (Redundantly stored, as the reserve record also has this info.) + */ + exchangeBaseUrl?: string; + + /** + * When was the withdrawal operation started started? + * Timestamp in milliseconds. + */ + timestampStart: DbPreciseTimestamp; + + /** + * When was the withdrawal operation completed? + */ + timestampFinish?: DbPreciseTimestamp; + + /** + * Current status of the reserve. + */ + status: WithdrawalGroupStatus; + + /** + * Restrict withdrawals from this reserve to this age. + */ + restrictAge?: number; + + /** + * Amount that was sent by the user to fund the reserve. + */ + instructedAmount?: AmountString; + + /** + * Amount that was observed when querying the reserve that + * we are withdrawing from. + * + * Useful for diagnostics. + */ + reserveBalanceAmount?: AmountString; + + /** + * Amount including fees (i.e. the amount subtracted from the + * reserve to withdraw all coins in this withdrawal session). + * + * (Initial amount confirmed by the user, might differ with denomSel + * on reselection.) + */ + rawWithdrawalAmount?: AmountString; + + /** + * Amount that will be added to the balance when the withdrawal succeeds. + * + * (Initial amount confirmed by the user, might differ with denomSel + * on reselection.) + */ + effectiveWithdrawalAmount?: AmountString; + + /** + * Denominations selected for withdrawal. + */ + denomsSel?: DenomSelectionState; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; +} + +export interface BankWithdrawUriRecord { + /** + * The withdraw URI we got from the bank. + */ + talerWithdrawUri: string; + + /** + * Reserve that was created for the withdraw URI. + */ + reservePub: string; +} + +/** + * Status of recoup operations that were grouped together. + * + * The remaining amount of involved coins should be set to zero + * in the same transaction that inserts the RecoupGroupRecord. + */ +export interface RecoupGroupRecord { + /** + * Unique identifier for the recoup group record. + */ + recoupGroupId: string; + + exchangeBaseUrl: string; + + operationStatus: RecoupOperationStatus; + + timestampStarted: DbPreciseTimestamp; + + timestampFinished: DbPreciseTimestamp | undefined; + + /** + * Public keys that identify the coins being recouped + * as part of this session. + * + * (Structured like this to enable multiEntry indexing in IndexedDB.) + */ + coinPubs: string[]; + + /** + * Array of flags to indicate whether the recoup finished on each individual coin. + */ + recoupFinishedPerCoin: boolean[]; + + /** + * Public keys of coins that should be scheduled for refreshing + * after all individual recoups are done. + */ + scheduleRefreshCoins: CoinRefreshRequest[]; +} + +export interface DepositTrackingInfo { + // Raw wire transfer identifier of the deposit. + wireTransferId: string; + // When was the wire transfer given to the bank. + timestampExecuted: DbProtocolTimestamp; + // Total amount transfer for this wtid (including fees) + amountRaw: AmountString; + // Wire fee amount for this exchange + wireFee: AmountString; + + exchangePub: string; +} + +export interface DepositInfoPerExchange { + /** + * Expected effective amount that will be deposited + * from coins of this exchange. + */ + amountEffective: AmountString; +} + +/** + * Group of deposits made by the wallet. + */ +export interface DepositGroupRecord { + depositGroupId: string; + + currency: string; + + /** + * Instructed amount. + */ + amount: AmountString; + + wireTransferDeadline: DbProtocolTimestamp; + + merchantPub: string; + merchantPriv: string; + + noncePriv: string; + noncePub: string; + + /** + * Wire information used by all deposits in this + * deposit group. + */ + wire: { + payto_uri: string; + salt: string; + }; + + contractTermsHash: string; + + payCoinSelection?: DbCoinSelection; + + payCoinSelectionUid?: string; + + totalPayCost: AmountString; + + /** + * The counterparty effective deposit amount. + */ + counterpartyEffectiveDepositAmount: AmountString; + + timestampCreated: DbPreciseTimestamp; + + timestampFinished: DbPreciseTimestamp | undefined; + + /** + * When did the wallet last try a deposit request? + */ + timestampLastDepositAttempt: DbPreciseTimestamp | undefined; + + operationStatus: DepositOperationStatus; + + statusPerCoin?: DepositElementStatus[]; + + infoPerExchange?: Record<string, DepositInfoPerExchange>; + + /** + * When the deposit transaction was aborted and + * refreshes were tried, we create a refresh + * group and store the ID here. + */ + abortRefreshGroupId?: string; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; + + kycInfo?: DepositKycInfo; + kycAuthTransferOptions?: TransferOptionRaw[]; + kycAuthTransferExpiry?: TalerProtocolTimestamp; + + // FIXME: Do we need this and should it be in this object store? + trackingState?: { + [signature: string]: DepositTrackingInfo; + }; +} + +export interface DepositKycInfo { + accessToken?: string; + paytoHash: string; + exchangeBaseUrl: string; + lastCheckStatus?: number | undefined; + lastCheckCode?: number | undefined; + lastRuleGen?: number | undefined; + lastAmlReview?: boolean | undefined; + lastDeny?: DbPreciseTimestamp | undefined; + lastBadKycAuth?: boolean; +} + +export interface TombstoneRecord { + /** + * Tombstone ID, with the syntax "tmb:<type>:<key>". + */ + id: string; +} + +export interface DbPeerPushPaymentCoinSelection { + contributions: AmountString[]; + coinPubs: CoinPublicKeyString[]; +} + +/** + * Record for a push P2P payment that this wallet initiated. + */ +export interface PeerPushDebitRecord { + /** + * What exchange are funds coming from? + */ + exchangeBaseUrl: string; + + /** + * Restricted scope for this transaction. + * + * Relevant for coin reselection. + */ + restrictScope?: ScopeInfo; + + /** + * Instructed amount. + */ + amount: AmountString; + + /** + * Effective amount. + * + * (Called totalCost for historical reasons.) + */ + totalCost: AmountString; + + coinSel?: DbPeerPushPaymentCoinSelection; + + contractTermsHash: HashCodeString; + + /** + * Purse public key. Used as the primary key to look + * up this record. + */ + pursePub: string; + + /** + * Purse private key. + */ + pursePriv: string; + + /** + * Public key of the merge capability of the purse. + */ + mergePub: string; + + /** + * Private key of the merge capability of the purse. + */ + mergePriv: string; + + contractPriv: string; + contractPub: string; + + /** + * 24 byte nonce. + */ + contractEncNonce: string; + + purseExpiration: DbProtocolTimestamp; + + timestampCreated: DbPreciseTimestamp; + + abortRefreshGroupId?: string; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; + + /** + * Status of the peer push payment initiation. + */ + status: PeerPushDebitStatus; +} + +export interface PeerPullCreditRecord { + /** + * What exchange are we using for the payment request? + */ + exchangeBaseUrl: string; + + /** + * Amount requested. + * FIXME: What type of instructed amount is i? + */ + amount: AmountString; + + estimatedAmountEffective: AmountString; + + /** + * Purse public key. Used as the primary key to look + * up this record. + */ + pursePub: string; + + /** + * Purse private key. + */ + pursePriv: string; + + /** + * Hash of the contract terms. Also + * used to look up the contract terms in the DB. + */ + contractTermsHash: string; + + mergePub: string; + mergePriv: string; + + contractPub: string; + contractPriv: string; + + contractEncNonce: string; + + mergeTimestamp: DbPreciseTimestamp; + + mergeReserveRowId: number; + + /** + * Status of the peer pull payment initiation. + */ + status: PeerPullPaymentCreditStatus; + + kycPaytoHash?: string; + + kycAccessToken?: string; + + kycLastCheckStatus?: number | undefined; + kycLastCheckCode?: number | undefined; + kycLastRuleGen?: number | undefined; + kycLastAmlReview?: boolean | undefined; + kycLastDeny?: DbPreciseTimestamp | undefined; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; + + withdrawalGroupId: string | undefined; +} + +/** + * Record for a push P2P payment that this wallet was offered. + * + * Unique: (exchangeBaseUrl, pursePub) + */ +export interface PeerPushCreditRecord { + peerPushCreditId: string; + + exchangeBaseUrl: string; + + pursePub: string; + + mergePriv: string; + + contractPriv: string; + + timestamp: DbPreciseTimestamp; + + estimatedAmountEffective: AmountString; + + /** + * Hash of the contract terms. Also + * used to look up the contract terms in the DB. + */ + contractTermsHash: string; + + /** + * Status of the peer push payment incoming initiation. + */ + status: PeerPushCreditStatus; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; + + /** + * Associated withdrawal group. + */ + withdrawalGroupId: string | undefined; + + /** + * Currency of the peer push payment credit transaction. + * + * Mandatory in current schema version, optional for compatibility + * with older (ver_minor<4) DB versions. + */ + currency: string | undefined; + + kycPaytoHash?: string; + + kycAccessToken?: string; + + kycLastCheckStatus?: number | undefined; + kycLastCheckCode?: number | undefined; + kycLastRuleGen?: number | undefined; + kycLastAmlReview?: boolean | undefined; + kycLastDeny?: DbPreciseTimestamp | undefined; +} + +export interface PeerPullPaymentCoinSelection { + contributions: AmountString[]; + coinPubs: CoinPublicKeyString[]; + + /** + * Total cost based on the coin selection. + * Non undefined after status === "Accepted" + */ + totalCost: AmountString | undefined; +} + +/** + * AKA PeerPullDebit. + */ +export interface PeerPullPaymentIncomingRecord { + peerPullDebitId: string; + + pursePub: string; + + exchangeBaseUrl: string; + + amount: AmountString; + + contractTermsHash: string; + + timestampCreated: DbPreciseTimestamp; + + /** + * Contract priv that we got from the other party. + */ + contractPriv: string; + + /** + * Status of the peer push payment incoming initiation. + */ + status: PeerPullDebitRecordStatus; + + /** + * Estimated total cost when the record was created. + */ + totalCostEstimated: AmountString; + + abortRefreshGroupId?: string; + + abortReason?: TalerErrorDetail; + failReason?: TalerErrorDetail; + + coinSel?: PeerPullPaymentCoinSelection; +} + +/** + * Store for extra information about a reserve. + * + * Mostly used to store the private key for a reserve and to allow + * other records to reference the reserve key pair via a small row ID. + * + * In the future, we might also store KYC info about a reserve here. + * + * FIXME: Should reference exchange. + */ +export interface ReserveRecord { + rowId?: number; + + reservePub: string; + + reservePriv: string; + + status?: ReserveRecordStatus; + + requirementRow?: number; + + /** + * Balance threshold that we're currently requesting KYC for. + */ + thresholdRequested?: AmountString; + + /** + * Balance threshold that we already have passed KYC for. + */ + thresholdGranted?: AmountString; + + /** + * Threshold that will trigger the next KYC. + */ + thresholdNext?: AmountString; + + kycAccessToken?: string; + + amlReview?: boolean; +} + +export interface OperationRetryRecord { + /** + * Unique identifier for the operation. Typically of + * the format `${opType}-${opUniqueKey}` + * + * @see {@link TaskIdentifiers} + */ + id: string; + + lastError?: TalerErrorDetail; + + retryInfo: DbRetryInfo; +} + +/** + * Availability of coins of a given denomination (and age restriction!). + * + * We can't store this information with the denomination record, as one denomination + * can be withdrawn with multiple age restrictions. + */ +export interface CoinAvailabilityRecord { + currency: string; + value: AmountString; + denomPubHash: string; + exchangeBaseUrl: string; + exchangeMasterPub?: string; + + /** + * Age restriction on the coin, or 0 for no age restriction (or + * denomination without age restriction support). + */ + maxAge: number; + + /** + * Number of fresh coins of this denomination that are available. + */ + freshCoinCount: number; + + /** + * Number of fresh coins that are available + * and visible, i.e. the source transaction is in + * a final state. + */ + visibleCoinCount: number; + + /** + * Number of coins that we expect to obtain via a pending refresh. + */ + pendingRefreshOutputCount?: number; +} + +export interface ContractTermsRecord { + /** + * Contract terms hash. + */ + h: string; + + /** + * Contract terms JSON. + */ + contractTermsRaw: any; +} + +export interface DbExchangeHandle { + url: string; + exchangeMasterPub: string; +} + +export interface DbAuditorHandle { + url: string; + auditorPub: string; +} + +/** + * Metadata about a group of refunds with the merchant. + */ +export interface RefundGroupRecord { + status: RefundGroupStatus; + + /** + * Timestamp when the refund group was created. + */ + timestampCreated: DbPreciseTimestamp; + + proposalId: string; + + refundGroupId: string; + + refreshGroupId?: string; + + amountRaw: AmountString; + + /** + * Estimated effective amount, based on + * refund fees and refresh costs. + */ + amountEffective: AmountString; +} + +/** + * Refund for a single coin in a payment with a merchant. + */ +export interface RefundItemRecord { + /** + * Auto-increment DB record ID. + */ + id?: number; + + status: RefundItemStatus; + + /** + * Mandatory since DB minor version 15. + */ + proposalId?: string; + + refundGroupId: string; + + /** + * Execution time as claimed by the merchant + */ + executionTime: DbProtocolTimestamp; + + /** + * Time when the wallet became aware of the refund. + */ + obtainedTime: DbPreciseTimestamp; + + refundAmount: AmountString; + + coinPub: string; + + rtxid: number; +} + +export function passthroughCodec<T>(): Codec<T> { + return codecForAny(); +} + +export interface GlobalCurrencyAuditorRecord { + id?: number; + currency: string; + auditorBaseUrl: string; + auditorPub: string; +} + +export interface GlobalCurrencyExchangeRecord { + id?: number; + currency: string; + exchangeBaseUrl: string; + exchangeMasterPub: string; +} + +/** + * Metadata for a transaction. + * This object store is effectively a materialzed view of transactions gathered + * from various other object stores. + * + * Primary key: transactionId + */ +export interface TransactionMetaRecord { + /** + * Transaction identifier. + * Also determines the type of the transaction. + */ + transactionId: string; + + timestamp: DbPreciseTimestamp; + + /** + * Status of the transaction, matches the status enum of the + * transaction of the type determined by the transaction ID. + */ + status: number; + + /** + * Exchanges involved in the transaction. + */ + exchanges: string[]; + + currency: string; +} + +export interface DenomLossEventRecord { + denomLossEventId: string; + currency: string; + denomPubHashes: string[]; + status: DenomLossStatus; + timestampCreated: DbPreciseTimestamp; + amount: string; + eventType: DenomLossEventType; + exchangeBaseUrl: string; +} + +export interface CurrencyInfoRecord { + /** + * Stringified scope info. + */ + scopeInfoStr: string; + + /** + * Currency specification. + */ + currencySpec: CurrencySpecification; + + /** + * How did the currency info get set? + */ + source: "exchange" | "user" | "preset"; +} + +export enum ExchangeMigrationReason { + MismatchedBaseUrl = "mismatched-base-url", + UnavailableOldUrl = "unavailable-old-url", +} + +export interface ExchangeMigrationLogRecord { + oldExchangeBaseUrl: string; + newExchangeBaseUrl: string; + timestamp: DbPreciseTimestamp; + /** + * Reason that triggered the exchange base URL migration. + */ + reason: ExchangeMigrationReason; +} + +export interface ExchangeBaseUrlFixupRecord { + exchangeBaseUrl: string; + replacement: string; +} + +/** + * Record for donation planchets. + */ +export interface DonationPlanchetRecord { + donauBaseUrl: string; + udiNonce: HashCodeString; + donorTaxIdHash: HashCodeString; + donorHashSalt: string; + donorTaxId: string; + donationYear: number; + proposalId: string; + /** Index of this udi within the selected donation units for the purchase. */ + udiIndex: number; + blindedUdi: BlindedUniqueDonationIdentifier; + /** blinding key secret */ + bks: string; + donationUnitPubHash: HashCodeString; + value: AmountString; +} + +/** + * Record for donation receipts. + */ +export interface DonationReceiptRecord { + status: DonationReceiptStatus; + donauBaseUrl: string; + udiNonce: HashCodeString; + proposalId: string; + donationYear: number; + donationUnitPubHash: HashCodeString; + donationUnitSig: DonationReceiptSignature; + donorTaxIdHash: HashCodeString; + donorHashSalt: string; + donorTaxId: string; + value: AmountString; + /** Index of this udi within the selected donation units for the purchase. */ + udiIndex: number; +} + +export interface DonationSummaryRecord { + donauBaseUrl: string; + legalDomain?: string; + year: number; + currency: string; + amountReceiptsAvailable: AmountString; + amountReceiptsSubmitted: AmountString; +} + +/** + * Record for contacts + */ +export interface ContactRecord { + /** + * The mailbox URI of this contact + */ + mailboxBaseUri: string; + + /** + * The mailbox identity + */ + mailboxAddress: HashCodeString; + + /** + * The alias of this contact + */ + alias: string; + + /** + * The type of the alias + */ + aliasType: string; + + /** + * The source of this alias + */ + source: string; + + /** + * The local petname of this alias + */ + petname: string; +} + +/** + * Schema definition for the IndexedDB + * wallet database. + */ +export const WalletIndexedDbStoresV1 = { + exchangeBaseUrlMigrationLog: describeStoreV2({ + recordCodec: passthroughCodec<ExchangeMigrationLogRecord>(), + storeName: "exchangeBaseUrlMigrationLog", + keyPath: ["oldExchangeBaseUrl", "newExchangeBaseUrl"], + versionAdded: 18, + indexes: {}, + }), + exchangeBaseUrlFixups: describeStoreV2({ + recordCodec: passthroughCodec<ExchangeBaseUrlFixupRecord>(), + storeName: "exchangeBaseUrlFixups", + keyPath: "exchangeBaseUrl", + versionAdded: 19, + indexes: {}, + }), + denomLossEvents: describeStoreV2({ + recordCodec: passthroughCodec<DenomLossEventRecord>(), + storeName: "denomLossEvents", + keyPath: "denomLossEventId", + versionAdded: 9, + indexes: { + byCurrency: describeIndex("byCurrency", "currency", { + versionAdded: 9, + }), + byStatus: describeIndex("byStatus", "status", { + versionAdded: 10, + }), + }, + }), + transactionsMeta: describeStoreV2({ + recordCodec: passthroughCodec<TransactionMetaRecord>(), + storeName: "transactionsMeta", + keyPath: "transactionId", + versionAdded: 13, + indexes: { + byCurrency: describeIndex("byCurrency", "currency", { + versionAdded: 13, + }), + byExchange: describeIndex("byExchange", "exchanges", { + versionAdded: 13, + multiEntry: true, + }), + byTimestamp: describeIndex("byTimestamp", "timestamp", { + versionAdded: 13, + }), + byStatus: describeIndex("byStatus", "status", { + versionAdded: 13, + }), + }, + }), + currencyInfo: describeStoreV2({ + recordCodec: passthroughCodec<CurrencyInfoRecord>(), + storeName: "currencyInfo", + keyPath: "scopeInfoStr", + versionAdded: 12, + }), + globalCurrencyAuditors: describeStoreV2({ + recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(), + storeName: "globalCurrencyAuditors", + keyPath: "id", + autoIncrement: true, + versionAdded: 3, + indexes: { + byCurrencyAndUrlAndPub: describeIndex( + "byCurrencyAndUrlAndPub", + ["currency", "auditorBaseUrl", "auditorPub"], + { + unique: true, + versionAdded: 4, + }, + ), + }, + }), + globalCurrencyExchanges: describeStoreV2({ + recordCodec: passthroughCodec<GlobalCurrencyExchangeRecord>(), + storeName: "globalCurrencyExchanges", + keyPath: "id", + autoIncrement: true, + versionAdded: 3, + indexes: { + byCurrencyAndUrlAndPub: describeIndex( + "byCurrencyAndUrlAndPub", + ["currency", "exchangeBaseUrl", "exchangeMasterPub"], + { + unique: true, + versionAdded: 4, + }, + ), + }, + }), + coinAvailability: describeStore( + "coinAvailability", + describeContents<CoinAvailabilityRecord>({ + keyPath: ["exchangeBaseUrl", "denomPubHash", "maxAge"], + }), + { + byExchangeAgeAvailability: describeIndex("byExchangeAgeAvailability", [ + "exchangeBaseUrl", + "maxAge", + "freshCoinCount", + ]), + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 8, + }), + }, + ), + coinHistory: describeStoreV2({ + storeName: "coinHistory", + recordCodec: passthroughCodec<CoinHistoryRecord>(), + keyPath: "coinPub", + versionAdded: 11, + }), + coins: describeStore( + "coins", + describeContents<CoinRecord>({ + keyPath: "coinPub", + }), + { + byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"), + byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"), + byExchangeDenomPubHashAndAgeAndStatus: describeIndex( + "byExchangeDenomPubHashAndAgeAndStatus", + ["exchangeBaseUrl", "denomPubHash", "maxAge", "status"], + ), + byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"), + bySourceTransactionId: describeIndex( + "bySourceTransactionId", + "sourceTransactionId", + { + versionAdded: 9, + }, + ), + }, + ), + tokens: describeStore( + "tokens", + describeContents<TokenRecord>({ + keyPath: "tokenUsePub", + versionAdded: 16, + }), + { + byTokenIssuePubHash: describeIndex( + "byTokenIssuePubHash", + "tokenIssuePubHash", + { + versionAdded: 17, + }, + ), + byPurchaseIdAndChoiceIndex: describeIndex( + "byPurchaseIdAndChoiceIndex", + ["purchaseId", "choiceIndex"], + { + versionAdded: 17, + }, + ), + byTokenFamilyHash: describeIndex("byTokenFamilyHash", "tokenFamilyHash", { + versionAdded: 21, + }), + }, + ), + slates: describeStore( + "slates", + describeContents<SlateRecord>({ + keyPath: "tokenUsePub", + versionAdded: 16, + }), + { + byPurchaseIdAndChoiceIndex: describeIndex( + "byPurchaseIdAndChoiceIndex", + ["purchaseId", "choiceIndex"], + { + versionAdded: 17, + }, + ), + byPurchaseIdAndChoiceIndexAndOutputIndex: describeIndex( + "byPurchaseIdAndChoiceIndexAndOutputIndex", + ["purchaseId", "choiceIndex", "outputIndex"], + { + versionAdded: 17, + }, + ), + byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex: describeIndex( + "byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex", + ["purchaseId", "choiceIndex", "outputIndex", "repeatIndex"], + { + versionAdded: 29, + }, + ), + }, + ), + reserves: describeStore( + "reserves", + describeContents<ReserveRecord>({ + keyPath: "rowId", + autoIncrement: true, + }), + { + byReservePub: describeIndex("byReservePub", "reservePub", {}), + }, + ), + config: describeStore( + "config", + describeContents<ConfigRecord>({ keyPath: "key" }), + {}, + ), + denominations: describeStore( + "denominations", + describeContents<DenominationRecord>({ + keyPath: ["exchangeBaseUrl", "denomPubHash"], + }), + { + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl"), + byVerificationStatus: describeIndex( + "byVerificationStatus", + "verificationStatus", + { + versionAdded: 26, + }, + ), + byDenominationFamilySerialAndStampExpireWithdraw: describeIndex( + "byDenominationFamilySerialAndStampExpireWithdraw", + ["denominationFamilySerial", "stampExpireWithdraw"], + { + versionAdded: 27, + }, + ), + }, + ), + denominationFamilies: describeStore( + "denominationFamilies", + describeContents<DenominationFamilyRecord>({ + keyPath: "denominationFamilySerial", + versionAdded: 27, + autoIncrement: true, + }), + { + byExchangeBaseUrl: describeIndex( + "byExchangeBaseUrl", + "familyParams.exchangeBaseUrl", + { + versionAdded: 27, + }, + ), + byFamilyParms: describeIndex( + "byFamilyParams", + [ + "familyParams.exchangeBaseUrl", + "familyParams.exchangeMasterPub", + "familyParams.value", + "familyParams.feeWithdraw", + "familyParams.feeDeposit", + "familyParams.feeRefresh", + "familyParams.feeRefund", + ], + { + versionAdded: 28, + }, + ), + // Reserved legacy index names: + // * byFamilyParamsHash + }, + ), + exchanges: describeStore( + "exchanges", + describeContents<ExchangeEntryRecord>({ + keyPath: "baseUrl", + }), + {}, + ), + exchangeDetails: describeStore( + "exchangeDetails", + describeContents<ExchangeDetailsRecord>({ + keyPath: "rowId", + autoIncrement: true, + }), + { + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 2, + }), + byPointer: describeIndex( + "byDetailsPointer", + ["exchangeBaseUrl", "currency", "masterPublicKey"], + { + unique: true, + }, + ), + }, + ), + exchangeSignKeys: describeStore( + "exchangeSignKeys", + describeContents<ExchangeSignkeysRecord>({ + keyPath: ["exchangeDetailsRowId", "signkeyPub"], + }), + { + // Caution: By historical accident, the index is over an array. + byExchangeDetailsRowId: describeIndex("byExchangeDetailsRowId", [ + "exchangeDetailsRowId", + ]), + }, + ), + contacts: describeStoreV2({ + recordCodec: passthroughCodec<ContactRecord>(), + storeName: "contacts", + keyPath: ["alias", "aliasType"], + indexes: {}, + versionAdded: 24, + }), + mailboxMessages: describeStoreV2({ + recordCodec: passthroughCodec<MailboxMessageRecord>(), + storeName: "mailboxMessages", + keyPath: ["originMailboxBaseUrl", "talerUri"], + indexes: {}, + versionAdded: 24, + }), + mailboxConfigurations: describeStoreV2({ + recordCodec: passthroughCodec<MailboxConfiguration>(), + storeName: "mailboxConfigurations", + keyPath: "mailboxBaseUrl", + indexes: {}, + versionAdded: 24, + }), + refreshGroups: describeStore( + "refreshGroups", + describeContents<RefreshGroupRecord>({ + keyPath: "refreshGroupId", + }), + { + byStatus: describeIndex("byStatus", "operationStatus"), + byOriginatingTransactionId: describeIndex( + "byOriginatingTransactionId", + "originatingTransactionId", + { + versionAdded: 5, + }, + ), + }, + ), + refreshSessions: describeStore( + "refreshSessions", + describeContents<RefreshSessionRecord>({ + keyPath: ["refreshGroupId", "coinIndex"], + }), + { + byRefreshGroupId: describeIndex("byRefreshGroupId", "refreshGroupId", { + versionAdded: 15, + }), + }, + ), + recoupGroups: describeStore( + "recoupGroups", + describeContents<RecoupGroupRecord>({ + keyPath: "recoupGroupId", + }), + { + byStatus: describeIndex("byStatus", "operationStatus", { + versionAdded: 6, + }), + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 15, + }), + }, + ), + purchases: describeStore( + "purchases", + describeContents<PurchaseRecord>({ keyPath: "proposalId" }), + { + byStatus: describeIndex("byStatus", "purchaseStatus"), + byFulfillmentUrl: describeIndex( + "byFulfillmentUrl", + "download.fulfillmentUrl", + ), + byUrlAndOrderId: describeIndex("byUrlAndOrderId", [ + "merchantBaseUrl", + "orderId", + ]), + byExchange: describeIndex("byExchange", "exchanges", { + versionAdded: 15, + multiEntry: true, + }), + }, + ), + donationPlanchets: describeStoreV2({ + recordCodec: passthroughCodec<DonationPlanchetRecord>(), + storeName: "donationPlanchets", + keyPath: "udiNonce", + versionAdded: 20, + indexes: { + byProposalId: describeIndex("byProposalId", "proposalId", { + versionAdded: 20, + }), + }, + }), + donationReceipts: describeStoreV2({ + recordCodec: passthroughCodec<DonationReceiptRecord>(), + storeName: "donationReceipts", + keyPath: "udiNonce", + versionAdded: 20, + indexes: { + byStatus: describeIndex("byStatus", "status", { + versionAdded: 20, + }), + byDonauBaseUrl: describeIndex("byDonauBaseUrl", "donauBaseUrl", { + versionAdded: 23, + }), + byStatusAndDonauBaseUrl: describeIndex( + "byStatusAndDonauBaseUrl", + ["status", "donauBaseUrl"], + { + versionAdded: 23, + }, + ), + }, + }), + donationSummaries: describeStoreV2({ + recordCodec: passthroughCodec<DonationSummaryRecord>(), + storeName: "donationSummaries", + keyPath: ["donauBaseUrl", "year", "currency"], + versionAdded: 22, + indexes: {}, + }), + withdrawalGroups: describeStore( + "withdrawalGroups", + describeContents<WithdrawalGroupRecord>({ + keyPath: "withdrawalGroupId", + }), + { + byStatus: describeIndex("byStatus", "status"), + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 2, + }), + byTalerWithdrawUri: describeIndex( + "byTalerWithdrawUri", + "wgInfo.bankInfo.talerWithdrawUri", + ), + }, + ), + planchets: describeStore( + "planchets", + describeContents<PlanchetRecord>({ keyPath: "coinPub" }), + { + byGroupAndIndex: describeIndex( + "byGroupAndIndex", + ["withdrawalGroupId", "coinIdx"], + { + unique: true, + }, + ), + byGroup: describeIndex("byGroup", "withdrawalGroupId"), + byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"), + }, + ), + bankWithdrawUris: describeStore( + "bankWithdrawUris", + describeContents<BankWithdrawUriRecord>({ + keyPath: "talerWithdrawUri", + }), + { + byGroup: describeIndex("byGroup", "withdrawalGroupId"), + }, + ), + depositGroups: describeStore( + "depositGroups", + describeContents<DepositGroupRecord>({ + keyPath: "depositGroupId", + }), + { + byStatus: describeIndex("byStatus", "operationStatus"), + }, + ), + tombstones: describeStore( + "tombstones", + describeContents<TombstoneRecord>({ keyPath: "id" }), + {}, + ), + operationRetries: describeStore( + "operationRetries", + describeContents<OperationRetryRecord>({ + keyPath: "id", + }), + {}, + ), + peerPushCredit: describeStore( + "peerPushCredit", + describeContents<PeerPushCreditRecord>({ + keyPath: "peerPushCreditId", + }), + { + byExchangeAndPurse: describeIndex("byExchangeAndPurse", [ + "exchangeBaseUrl", + "pursePub", + ]), + byExchangeAndContractPriv: describeIndex( + "byExchangeAndContractPriv", + ["exchangeBaseUrl", "contractPriv"], + { + unique: true, + }, + ), + byWithdrawalGroupId: describeIndex( + "byWithdrawalGroupId", + "withdrawalGroupId", + {}, + ), + byStatus: describeIndex("byStatus", "status"), + }, + ), + peerPullDebit: describeStore( + "peerPullDebit", + describeContents<PeerPullPaymentIncomingRecord>({ + keyPath: "peerPullDebitId", + }), + { + byExchangeAndPurse: describeIndex("byExchangeAndPurse", [ + "exchangeBaseUrl", + "pursePub", + ]), + byExchangeAndContractPriv: describeIndex( + "byExchangeAndContractPriv", + ["exchangeBaseUrl", "contractPriv"], + { + unique: true, + }, + ), + byStatus: describeIndex("byStatus", "status"), + }, + ), + peerPullCredit: describeStore( + "peerPullCredit", + describeContents<PeerPullCreditRecord>({ + keyPath: "pursePub", + }), + { + byStatus: describeIndex("byStatus", "status"), + byWithdrawalGroupId: describeIndex( + "byWithdrawalGroupId", + "withdrawalGroupId", + {}, + ), + }, + ), + peerPushDebit: describeStore( + "peerPushDebit", + describeContents<PeerPushDebitRecord>({ + keyPath: "pursePub", + }), + { + byStatus: describeIndex("byStatus", "status"), + }, + ), + bankAccountsV2: describeStore( + "bankAccountsV2", + describeContents<BankAccountsRecord>({ + keyPath: "bankAccountId", + versionAdded: 14, + }), + { + byPaytoUri: describeIndex("byPaytoUri", "paytoUri", { + versionAdded: 14, + }), + }, + ), + contractTerms: describeStore( + "contractTerms", + describeContents<ContractTermsRecord>({ + keyPath: "h", + }), + {}, + ), + refundGroups: describeStore( + "refundGroups", + describeContents<RefundGroupRecord>({ + keyPath: "refundGroupId", + }), + { + byProposalId: describeIndex("byProposalId", "proposalId"), + byStatus: describeIndex("byStatus", "status", {}), + }, + ), + refundItems: describeStore( + "refundItems", + describeContents<RefundItemRecord>({ + keyPath: "id", + autoIncrement: true, + }), + { + byCoinPubAndRtxid: describeIndex("byCoinPubAndRtxid", [ + "coinPub", + "rtxid", + ]), + // FIXME: Why is this a list of index keys? Confusing! + byRefundGroupId: describeIndex("byRefundGroupId", ["refundGroupId"]), + }, + ), + fixups: describeStore( + "fixups", + describeContents<FixupRecord>({ + keyPath: "fixupName", + }), + {}, + ), + // + // Obsolete stores, not used anymore + // + obsolete_backupProviders: describeStore( + "backupProviders", + describeContents<unknown>({ + keyPath: "baseUrl", + }), + { + byPaymentProposalId: describeIndex( + "byPaymentProposalId", + "paymentProposalIds", + { + multiEntry: true, + }, + ), + }, + ), + _obsolete_transactions: describeStoreV2({ + recordCodec: passthroughCodec<unknown>(), + storeName: "transactions", + keyPath: "transactionItem.transactionId", + versionAdded: 7, + indexes: { + byCurrency: describeIndex("byCurrency", "currency", { + versionAdded: 7, + }), + byExchange: describeIndex("byExchange", "exchanges", { + versionAdded: 7, + multiEntry: true, + }), + }, + }), + _obsolete_bankAccounts: describeStore( + "bankAccounts", + describeContents<any>({ + keyPath: "uri", + }), + {}, + ), + _obsolete_rewards: describeStore( + "rewards", + describeContents<any>({ keyPath: "walletRewardId" }), + { + byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [ + "merchantRewardId", + "merchantBaseUrl", + ]), + byStatus: describeIndex("byStatus", "status", { + versionAdded: 8, + }), + }, + ), + obsolete_userAttention: describeStore( + "userAttention", + describeContents<unknown>({ + keyPath: ["entityId", "info.type"], + }), + {}, + ), +}; + +export type WalletIndexedDbTransaction = DbReadWriteTransaction< + typeof WalletIndexedDbStoresV1, + Array<StoreNames<typeof WalletIndexedDbStoresV1>> +>; + +/** + * An applied migration. + */ +export interface FixupRecord { + fixupName: string; +} + +/** + * User accounts + */ +export interface BankAccountsRecord { + /** + * Opaque identifier for the bank account. + */ + bankAccountId: string; + + /** + * Payto URI of the bank account. + */ + paytoUri: string; + + /** + * User-defined label for the account. + */ + label: string | undefined; + + currencies: string[] | undefined; + + /** + * FIXME: Provide more info here. + */ + kycCompleted: boolean; +} + +export interface MetaConfigRecord { + key: string; + value: any; +} + +export const walletMetadataStore = { + metaConfig: describeStore( + "metaConfig", + describeContents<MetaConfigRecord>({ keyPath: "key" }), + {}, + ), +}; + +export interface StoredBackupMeta { + name: string; +} + +export const StoredBackupStores = { + backupMeta: describeStore( + "backupMeta", + describeContents<StoredBackupMeta>({ keyPath: "name" }), + {}, + ), + backupData: describeStore("backupData", describeContents<any>({}), {}), +}; + +export interface DbDumpRecord { + /** + * Key, serialized with structuredEncapsulated. + * + * Only present for out-of-line keys (i.e. no key path). + */ + key?: any; + /** + * Value, serialized with structuredEncapsulated. + */ + value: any; +} + +export interface DbIndexDump { + keyPath: string | string[]; + multiEntry: boolean; + unique: boolean; +} + +export interface DbStoreDump { + keyPath?: string | string[]; + autoIncrement: boolean; + indexes: { [indexName: string]: DbIndexDump }; + records: DbDumpRecord[]; +} + +export interface DbDumpDatabase { + version: number; + stores: { [storeName: string]: DbStoreDump }; +} + +export interface DbDump { + databases: { + [name: string]: DbDumpDatabase; + }; +} + +const logger = new Logger("db.ts"); + +export async function exportSingleDb( + idb: IDBFactory, + dbName: string, +): Promise<DbDumpDatabase> { + const myDb = await openDatabase( + idb, + dbName, + undefined, + () => { + logger.info(`unexpected onversionchange in exportSingleDb of ${dbName}`); + }, + () => { + logger.info(`unexpected onupgradeneeded in exportSingleDb of ${dbName}`); + }, + ); + + const singleDbDump: DbDumpDatabase = { + version: myDb.version, + stores: {}, + }; + + return new Promise((resolve, reject) => { + const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); + tx.addEventListener("complete", () => { + //myDb.close(); + resolve(singleDbDump); + }); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < myDb.objectStoreNames.length; i++) { + const name = myDb.objectStoreNames[i]; + const store = tx.objectStore(name); + const storeDump: DbStoreDump = { + autoIncrement: store.autoIncrement, + keyPath: store.keyPath, + indexes: {}, + records: [], + }; + const indexNames = store.indexNames; + for (let j = 0; j < indexNames.length; j++) { + const idxName = indexNames[j]; + const index = store.index(idxName); + storeDump.indexes[idxName] = { + keyPath: index.keyPath, + multiEntry: index.multiEntry, + unique: index.unique, + }; + } + singleDbDump.stores[name] = storeDump; + store.openCursor().addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + const rec: DbDumpRecord = { + value: structuredEncapsulate(cursor.value), + }; + // Only store key if necessary, i.e. when + // the key is not stored as part of the object via + // a key path. + if (store.keyPath == null) { + rec.key = structuredEncapsulate(cursor.key); + } + storeDump.records.push(rec); + cursor.continue(); + } + }); + } + }); +} + +export async function exportDb(idb: IDBFactory): Promise<DbDump> { + const dbDump: DbDump = { + databases: {}, + }; + + dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_META_DB_NAME, + ); + dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb( + idb, + TALER_WALLET_MAIN_DB_NAME, + ); + + return dbDump; +} + +async function recoverFromDump( + db: IDBDatabase, + dbDump: DbDumpDatabase, +): Promise<void> { + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + const txProm = promiseFromTransaction(tx); + const storeNames = db.objectStoreNames; + for (let i = 0; i < storeNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = dbDump.stores[name]; + if (!storeDump) continue; + await promiseFromRequest(tx.objectStore(name).clear()); + logger.info(`importing ${storeDump.records.length} records into ${name}`); + for (let rec of storeDump.records) { + await promiseFromRequest(tx.objectStore(name).put(rec.value, rec.key)); + logger.trace("importing record done"); + } + } + tx.commit(); + return await txProm; +} + +function checkDbDump(x: any): x is DbDump { + return "databases" in x; +} + +export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> { + const d = structuredRevive(dumpJson); + if (checkDbDump(d)) { + const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME]; + if (!walletDb) { + throw Error( + `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`, + ); + } + await recoverFromDump(db, walletDb); + } else { + throw Error("unable to import, doesn't look like a valid DB dump"); + } +} + +export interface FixupDescription { + name: string; + fn(tx: WalletIndexedDbTransaction): Promise<void>; +} + +/** + * Manual migrations between minor versions of the DB schema. + * + * Fixups *must* be idempotent. + */ +export const walletDbFixups: FixupDescription[] = [ + // Removing this would cause old transactions + // to show up under multiple exchanges + { + fn: fixup20250915TransactionsScope, + name: "fixup20250915TransactionsScope", + }, + // Removing this would cause merchant acceptable + // amount to be calculaed based on exchangeBaseUrl + // instead of masterPublicKey for old coins. + { + fn: fixupCoinAvailabilityExchangePub, + name: "fixupCoinAvailabilityExchangePub", + }, + // Can be removed once all affected refresh groups have + // been fixed. Conservative estimate: Jan 2028. + { + fn: fixup20260116BadRefreshCoinSelection, + name: "fixup20260116BadRefreshCoinSelection", + }, + // Denom families were introduced. + // This migration creates denom families + // for existing denomination records. + { + fn: fixup20260203DenomFamilyMigration, + name: "fixup20260203DenomFamilyMigration", + }, + // Fix a problem where refreshes went into a failed state + // instead of retrying. + { + fn: fixup20260213RefreshBlunder, + name: "fixup20260213RefreshBlunder", + }, +]; + +async function fixup20260213RefreshBlunder( + tx: WalletIndexedDbTransaction, +): Promise<void> { + const refreshes = await tx.refreshGroups.indexes.byStatus.getAll( + RefreshOperationStatus.Failed, + ); + for (const refreshGroup of refreshes) { + for ( + let coinIndex = 0; + coinIndex < refreshGroup.statusPerCoin.length; + coinIndex++ + ) { + let changed = false; + if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Failed) { + const rs = await tx.refreshSessions.get([ + refreshGroup.refreshGroupId, + coinIndex, + ]); + if ( + rs?.lastError?.code === + TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_EXPIRED + ) { + refreshGroup.statusPerCoin[coinIndex] = + RefreshCoinStatus.PendingRedenominate; + refreshGroup.operationStatus = + RefreshOperationStatus.PendingRedenominate; + delete refreshGroup.timestampFinished; + changed = true; + } + } + if (changed) { + await tx.refreshGroups.put(refreshGroup); + } + } + } +} + +async function fixup20260203DenomFamilyMigration( + tx: WalletIndexedDbTransaction, +): Promise<void> { + const batchSize = 500; + + let range: IDBKeyRange | undefined = undefined; + + while (1) { + const batch = await tx.denominations.getAll(range, batchSize); + + if (batch.length === 0) { + break; + } + + logger.info(`fixing up batch of ${batch.length} denominations`); + + const last = batch[batch.length - 1]; + range = GlobalIDB.KeyRange.lowerBound( + [last.exchangeBaseUrl, last.denomPubHash], + true, + ); + + for (const r of batch) { + const fp: DenomFamilyParams = { + exchangeBaseUrl: r.exchangeBaseUrl, + exchangeMasterPub: r.exchangeMasterPub, + feeDeposit: r.fees.feeDeposit, + feeRefresh: r.fees.feeRefresh, + feeRefund: r.fees.feeRefund, + feeWithdraw: r.fees.feeWithdraw, + value: r.value, + }; + if (r.denominationFamilySerial != null) { + // Fast path: Check if family exists and is correct. + const oldFpRec = await tx.denominationFamilies.get( + r.denominationFamilySerial, + ); + if ( + oldFpRec && + canonicalJson(fp) == canonicalJson(oldFpRec.familyParams) + ) { + continue; + } + } + const familyParamsIndexKey = [ + fp.exchangeBaseUrl, + fp.exchangeMasterPub, + fp.value, + fp.feeWithdraw, + fp.feeDeposit, + fp.feeRefresh, + fp.feeRefund, + ]; + const dfRec = + await tx.denominationFamilies.indexes.byFamilyParms.get( + familyParamsIndexKey, + ); + let denominationFamilySerial; + if (dfRec) { + denominationFamilySerial = dfRec.denominationFamilySerial; + } else { + const insRes = await tx.denominationFamilies.put({ + familyParams: fp, + }); + denominationFamilySerial = insRes.key; + } + checkDbInvariant( + typeof denominationFamilySerial == "number", + "denominationFamilySerial", + ); + r.denominationFamilySerial = denominationFamilySerial; + await tx.denominations.put(r); + } + } +} + +async function fixup20260116BadRefreshCoinSelection( + tx: WalletIndexedDbTransaction, +): Promise<void> { + await tx.refreshGroups.iter().forEachAsync(async (rec) => { + const inputAmount = Amounts.sumOrZero( + rec.currency, + rec.inputPerCoin, + ).amount; + const outputAmount = Amounts.sumOrZero( + rec.currency, + rec.expectedOutputPerCoin, + ).amount; + if (Amounts.isNonZero(inputAmount) && Amounts.isZero(outputAmount)) { + logger.info( + `fixing up refresh group ${rec.refreshGroupId}, setting status to PendingRedenominate`, + ); + rec.operationStatus = RefreshOperationStatus.PendingRedenominate; + delete rec.timestampFinished; + await tx.refreshGroups.put(rec); + } + }); +} + +/** + * Some old payment transactions didn't correctly + * set the involved exchanges. + * + * This fixup sets the exchanges of a payment transaction + * based on the coin selection. + */ +async function fixup20250915TransactionsScope( + tx: WalletIndexedDbTransaction, +): Promise<void> { + await tx.purchases.iter().forEachAsync(async (rec) => { + if ( + (rec.exchanges?.length ?? 0) == 0 && + rec.payInfo?.payCoinSelection != null + ) { + const pcs = rec.payInfo.payCoinSelection.coinPubs; + const exchSet: Set<string> = new Set(); + for (const pc in pcs) { + const coin = await tx.coins.get(pc); + if (!coin) { + continue; + } + exchSet.add(coin.exchangeBaseUrl); + } + rec.exchanges = [...exchSet]; + rec.exchanges.sort(); + if (rec.exchanges.length == 0) { + // For old SPURLOS transactions, set exchange manually + // when we can't infer it. + if ( + rec.timestamp <= 1736942400000_000 && + rec.download?.currency === "SPURLOS" + ) { + rec.exchanges = ["https://exchange.taler.datenspuren.de/"]; + } + logger.warn( + `unable to fix up pay transaction ${rec.proposalId}, could not reconstruct exchanges`, + ); + } + await tx.purchases.put(rec); + } + }); +} + +async function fixupCoinAvailabilityExchangePub( + tx: WalletIndexedDbTransaction, +): Promise<void> { + const cars = await tx.coinAvailability.getAll(); + const exchanges: Record<string, ExchangeDetailsRecord | undefined> = {}; + for (const car of cars) { + if (car.exchangeMasterPub === undefined) { + if (exchanges[car.exchangeBaseUrl] === undefined) { + exchanges[car.exchangeBaseUrl] = + await tx.exchangeDetails.indexes.byExchangeBaseUrl.get( + car.exchangeBaseUrl, + ); + } + + const exchange = exchanges[car.exchangeBaseUrl]; + if (exchange !== undefined) { + car.exchangeMasterPub = exchange.masterPublicKey; + await tx.coinAvailability.put(car); + } + } + } +} + +export async function applyFixups( + db: DbAccess<typeof WalletIndexedDbStoresV1>, +): Promise<number> { + logger.trace("applying fixups"); + let count = 0; + for (const fixupInstruction of walletDbFixups) { + await db.runAllStoresReadWriteTx({}, async (tx) => { + logger.trace(`checking fixup ${fixupInstruction.name}`); + const fixupRecord = await tx.fixups.get(fixupInstruction.name); + if (fixupRecord) { + return; + } + logger.trace(`applying DB fixup ${fixupInstruction.name}`); + await fixupInstruction.fn(tx); + await tx.fixups.put({ + fixupName: fixupInstruction.name, + }); + count++; + }); + } + return count; +} + +/** + * Upgrade an IndexedDB in an upgrade transaction. + * + * The upgrade is made based on a store map, i.e. the metadata + * structure that describes all the object stores and indexes. + */ +function upgradeFromStoreMap( + storeMap: any, // FIXME: nail down type + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +): void { + if (oldVersion === 0) { + for (const n in storeMap) { + const swi: StoreWithIndexes< + any, + StoreDescriptor<unknown>, + any + > = storeMap[n]; + const storeDesc: StoreDescriptor<unknown> = swi.store; + const s = db.createObjectStore(swi.storeName, { + autoIncrement: storeDesc.autoIncrement, + keyPath: storeDesc.keyPath, + }); + for (const indexName in swi.indexMap as any) { + const indexDesc: IndexDescriptor = swi.indexMap[indexName]; + s.createIndex(indexDesc.name, indexDesc.keyPath, { + multiEntry: indexDesc.multiEntry, + unique: indexDesc.unique, + }); + } + } + return; + } + if (oldVersion === newVersion) { + return; + } + logger.info(`upgrading database from ${oldVersion} to ${newVersion}`); + for (const n in storeMap) { + const swi: StoreWithIndexes<any, StoreDescriptor<unknown>, any> = storeMap[ + n + ]; + const storeDesc: StoreDescriptor<unknown> = swi.store; + const storeAddedVersion = storeDesc.versionAdded ?? 0; + let s: IDBObjectStore; + if (storeAddedVersion > oldVersion) { + // Be tolerant if object store already exists. + // Probably means somebody deployed without + // adding the "addedInVersion" attribute. + if (!upgradeTransaction.objectStoreNames.contains(swi.storeName)) { + try { + s = db.createObjectStore(swi.storeName, { + autoIncrement: storeDesc.autoIncrement, + keyPath: storeDesc.keyPath, + }); + } catch (e) { + const moreInfo = e instanceof Error ? ` Reason: ${e.message}` : ""; + throw new Error( + `Migration failed. Could not create store ${swi.storeName}.${moreInfo}`, + // @ts-expect-error no support for options.cause yet + { cause: e }, + ); + } + } + } + + s = upgradeTransaction.objectStore(swi.storeName); + + for (const indexName in swi.indexMap as any) { + const indexDesc: IndexDescriptor = swi.indexMap[indexName]; + const indexAddedVersion = indexDesc.versionAdded ?? 0; + if (indexAddedVersion <= oldVersion) { + continue; + } + // Be tolerant if index already exists. + // Probably means somebody deployed without + // adding the "addedInVersion" attribute. + if (!s.indexNames.contains(indexDesc.name)) { + try { + s.createIndex(indexDesc.name, indexDesc.keyPath, { + multiEntry: indexDesc.multiEntry, + unique: indexDesc.unique, + }); + } catch (e) { + const moreInfo = e instanceof Error ? ` Reason: ${e.message}` : ""; + throw Error( + `Migration failed. Could not create index ${indexDesc.name}/${indexDesc.keyPath}. ${moreInfo}`, + // @ts-expect-error no support for options.cause yet + { cause: e }, + ); + } + } + } + } +} + +function promiseFromTransaction(transaction: IDBTransaction): Promise<void> { + return new Promise<void>((resolve, reject) => { + transaction.oncomplete = () => { + resolve(); + }; + transaction.onerror = () => { + reject(); + }; + }); +} + +export function promiseFromRequest(request: IDBRequest): Promise<any> { + return new Promise((resolve, reject) => { + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; + }); +} + +/** + * Purge all data in the given database. + */ +export function clearDatabase(db: IDBDatabase): Promise<void> { + // db.objectStoreNames is a DOMStringList, so we need to convert + let stores: string[] = []; + for (let i = 0; i < db.objectStoreNames.length; i++) { + stores.push(db.objectStoreNames[i]); + } + logger.info(`clearing object stores: ${j2s(stores)}`); + const tx = db.transaction(stores, "readwrite"); + for (const store of stores) { + tx.objectStore(store).clear(); + } + return promiseFromTransaction(tx); +} + +function onTalerDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + WalletIndexedDbStoresV1, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +function onMetaDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + walletMetadataStore, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +function onStoredBackupsDbUpgradeNeeded( + db: IDBDatabase, + oldVersion: number, + newVersion: number, + upgradeTransaction: IDBTransaction, +) { + upgradeFromStoreMap( + StoredBackupStores, + db, + oldVersion, + newVersion, + upgradeTransaction, + ); +} + +export async function openStoredBackupsDatabase( + idbFactory: IDBFactory, +): Promise<DbAccess<typeof StoredBackupStores>> { + const backupsDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_STORED_BACKUPS_DB_NAME, + 1, + () => {}, + onStoredBackupsDbUpgradeNeeded, + ); + + const handle = new DbAccessImpl( + backupsDbHandle, + StoredBackupStores, + {}, + CancellationToken.CONTINUE, + ); + return handle; +} + +/** + * Return a promise that resolves + * to the taler wallet db. + * + * @param onVersionChange Called when another client concurrenctly connects to the database + * with a higher version. + */ +export async function openTalerDatabase( + idbFactory: IDBFactory, + onVersionChange: () => void, +): Promise<IDBDatabase> { + const metaDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_META_DB_NAME, + 1, + () => {}, + onMetaDbUpgradeNeeded, + ); + + const metaDb = new DbAccessImpl( + metaDbHandle, + walletMetadataStore, + {}, + CancellationToken.CONTINUE, + ); + let currentMainVersion: string | undefined; + await metaDb.runAllStoresReadWriteTx({}, async (tx) => { + const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY); + if (!dbVersionRecord) { + currentMainVersion = TALER_WALLET_MAIN_DB_NAME; + await tx.metaConfig.put({ + key: CURRENT_DB_CONFIG_KEY, + value: TALER_WALLET_MAIN_DB_NAME, + }); + } else { + currentMainVersion = dbVersionRecord.value; + } + }); + + if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) { + switch (currentMainVersion) { + case "taler-wallet-main-v2": + case "taler-wallet-main-v3": + case "taler-wallet-main-v4": // temporary, we might migrate v4 later + case "taler-wallet-main-v5": + case "taler-wallet-main-v6": + case "taler-wallet-main-v7": + case "taler-wallet-main-v8": + case "taler-wallet-main-v9": + // We consider this a pre-release + // development version, no migration is done. + await metaDb.runAllStoresReadWriteTx({}, async (tx) => { + await tx.metaConfig.put({ + key: CURRENT_DB_CONFIG_KEY, + value: TALER_WALLET_MAIN_DB_NAME, + }); + }); + break; + default: + throw Error( + `major migration from database major=${currentMainVersion} not supported`, + ); + } + } + + const mainDbHandle = await openDatabase( + idbFactory, + TALER_WALLET_MAIN_DB_NAME, + WALLET_DB_MINOR_VERSION, + onVersionChange, + onTalerDbUpgradeNeeded, + ); + + return mainDbHandle; +} + +export async function deleteTalerDatabase( + idbFactory: IDBFactory, +): Promise<void> { + return new Promise((resolve, reject) => { + const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME); + req.onerror = () => reject(req.error); + req.onsuccess = () => resolve(); + }); +} diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -1,4403 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-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/> - */ - -/** - * Imports. - */ -import { - Event, - GlobalIDB, - IDBDatabase, - IDBFactory, - IDBKeyRange, - IDBObjectStore, - IDBRequest, - IDBTransaction, - structuredEncapsulate, - structuredRevive, -} from "@gnu-taler/idb-bridge"; -import { - AbsoluteTime, - AccountLimit, - AgeCommitmentProof, - AmountString, - Amounts, - BlindedUniqueDonationIdentifier, - CancellationToken, - Codec, - CoinEnvelope, - CoinPublicKeyString, - CoinRefreshRequest, - CoinStatus, - CurrencySpecification, - DenomLossEventType, - DenomSelectionState, - DenominationInfo, - DenominationPubKey, - DonationReceiptSignature, - EddsaPublicKeyString, - EddsaSignatureString, - ExchangeAuditor, - ExchangeGlobalFees, - ExchangeRefundRequest, - HashCodeString, - Logger, - MailboxConfiguration, - MailboxMessageRecord, - MerchantContractTokenDetails, - MerchantContractTokenKind, - RefreshReason, - ScopeInfo, - SignedTokenEnvelope, - TalerErrorCode, - TalerErrorDetail, - TalerPreciseTimestamp, - TalerProtocolDuration, - TalerProtocolTimestamp, - TokenEnvelope, - TokenIssuePublicKey, - TokenUseSig, - TransactionIdStr, - TransferOptionRaw, - UnblindedDenominationSignature, - WireInfo, - WithdrawalExchangeAccountDetails, - ZeroLimitedOperation, - canonicalJson, - checkDbInvariant, - codecForAny, - encodeCrock, - hash, - j2s, - stringToBytes, -} from "@gnu-taler/taler-util"; -import { DbRetryInfo, TaskIdentifiers } from "./common.js"; -import { - DbAccess, - DbAccessImpl, - DbReadWriteTransaction, - IndexDescriptor, - StoreDescriptor, - StoreNames, - StoreWithIndexes, - describeContents, - describeIndex, - describeStore, - describeStoreV2, - openDatabase, -} from "./query.js"; - -/** - * This file contains the database schema of the Taler wallet together - * with some helper functions. - * - * Some design considerations: - * - By convention, each object store must have a corresponding "<Name>Record" - * interface defined for it. - * - For records that represent operations, there should be exactly - * one top-level enum field that indicates the status of the operation. - * This field should be present even if redundant, because the field - * will have an index. - * - Amounts are stored as strings, except when they are needed for - * indexing. - * - Every record that has a corresponding transaction item must have - * an index for a mandatory timestamp field. - * - Optional fields should be avoided, use "T | undefined" instead. - * - Do all records have some obvious, indexed field that can - * be used for range queries? - * - * @author Florian Dold <dold@taler.net> - */ - -/** - FIXMEs: - - Contract terms can be quite large. We currently tend to read the - full contract terms from the DB quite often. - Instead, we should probably extract what we need into a separate object - store. - - More object stores should have an "id" primary key, - as this makes referencing less expensive. - - Coin selections should probably go into a separate object store. - - Some records should be split up into an extra "details" record - that we don't always need to iterate over. - */ - -/** - * Name of the Taler database. This is effectively the major - * version of the DB schema. Whenever it changes, custom import logic - * for all previous versions must be written, which should be - * avoided. - */ -export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v10"; - -/** - * Name of the metadata database. This database is used - * to track major migrations of the main Taler database. - * - * (Minor migrations are handled via upgrade transactions.) - */ -export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; - -/** - * Name of the "stored backups" database. - * Stored backups are created before manually importing a backup. - * We use IndexedDB for this purpose, since we don't have file system - * access on some platforms. - */ -export const TALER_WALLET_STORED_BACKUPS_DB_NAME = - "taler-wallet-stored-backups"; - -/** - * Name of the "meta config" database. - */ -export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; - -/** - * Current database minor version, should be incremented - * each time we do minor schema changes on the database. - * A change is considered minor when fields are added in a - * backwards-compatible way or object stores and indices - * are added. - */ -export const WALLET_DB_MINOR_VERSION = 29; - -declare const symDbProtocolTimestamp: unique symbol; - -declare const symDbPreciseTimestamp: unique symbol; - -/** - * Timestamp, stored as microseconds. - * - * Always rounded to a full second. - */ -export type DbProtocolTimestamp = number & { [symDbProtocolTimestamp]: true }; - -/** - * Timestamp, stored as microseconds. - */ -export type DbPreciseTimestamp = number & { [symDbPreciseTimestamp]: true }; - -const DB_TIMESTAMP_FOREVER = Number.MAX_SAFE_INTEGER; - -export function timestampPreciseFromDb( - dbTs: DbPreciseTimestamp, -): TalerPreciseTimestamp { - return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000)); -} - -export function timestampOptionalPreciseFromDb( - dbTs: DbPreciseTimestamp | undefined, -): TalerPreciseTimestamp | undefined { - if (!dbTs) { - return undefined; - } - return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000)); -} - -export function timestampPreciseToDb( - stamp: TalerPreciseTimestamp, -): DbPreciseTimestamp { - if (stamp.t_s === "never") { - return DB_TIMESTAMP_FOREVER as DbPreciseTimestamp; - } else { - let tUs = stamp.t_s * 1000000; - if (stamp.off_us) { - tUs += stamp.off_us; - } - return tUs as DbPreciseTimestamp; - } -} - -export function timestampProtocolToDb( - stamp: TalerProtocolTimestamp, -): DbProtocolTimestamp { - if (stamp.t_s === "never") { - return DB_TIMESTAMP_FOREVER as DbProtocolTimestamp; - } else { - let tUs = stamp.t_s * 1000000; - return tUs as DbProtocolTimestamp; - } -} - -export function timestampProtocolFromDb( - stamp: DbProtocolTimestamp, -): TalerProtocolTimestamp { - return TalerProtocolTimestamp.fromSeconds(Math.floor(stamp / 1000000)); -} - -export function timestampAbsoluteFromDb( - stamp: DbProtocolTimestamp | DbPreciseTimestamp, -): AbsoluteTime { - if (stamp >= DB_TIMESTAMP_FOREVER) { - return AbsoluteTime.never(); - } - return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000)); -} - -export function timestampOptionalAbsoluteFromDb( - stamp: DbProtocolTimestamp | DbPreciseTimestamp | undefined, -): AbsoluteTime | undefined { - if (stamp == null) { - return undefined; - } - if (stamp >= DB_TIMESTAMP_FOREVER) { - return AbsoluteTime.never(); - } - return AbsoluteTime.fromMilliseconds(Math.floor(stamp / 1000)); -} - -export interface DenomFamilyParams { - exchangeBaseUrl: string; - exchangeMasterPub: string; - value: AmountString; - feeWithdraw: AmountString; - feeDeposit: AmountString; - feeRefresh: AmountString; - feeRefund: AmountString; -} - -/** - * Format of the operation status code: 0x0abc_nnnn - - * a=1: active - * 0x0100_nnnn: pending - * 0x0101_nnnn: dialog - * 0x0102_nnnn: (reserved) - * 0x0103_nnnn: aborting - * 0x0110_nnnn: suspended - * 0x0113_nnnn: suspended-aborting - * a=2: finalizing - * 0x0200_nnnn: finalizing - * 0x0210_nnnn: suspended-finalizing - * a=5: final - * 0x0500_nnnn: done - * 0x0501_nnnn: failed - * 0x0502_nnnn: expired - * 0x0503_nnnn: aborted - * - * nnnn=0000 should always be the most generic minor state for the major state - */ - -/** - * First possible operation status in the active range (inclusive). - */ -export const OPERATION_STATUS_NONFINAL_FIRST = 0x0100_0000; - -/** - * LAST possible operation status in the active range (inclusive). - */ -export const OPERATION_STATUS_NONFINAL_LAST = 0x0210_ffff; - -export const OPERATION_STATUS_DIALOG_FIRST = 0x0101_0000; -export const OPERATION_STATUS_DIALOG_LAST = 0x0101_ffff; - -export const OPERATION_STATUS_DONE_FIRST = 0x0500_0000; -export const OPERATION_STATUS_DONE_LAST = 0x0500_ffff; - -/** - * Status of a withdrawal. - */ -export enum WithdrawalGroupStatus { - /** - * Reserve must be registered with the bank. - */ - PendingRegisteringBank = 0x0100_0001, - SuspendedRegisteringBank = 0x0110_0001, - - /** - * We've registered reserve's information with the bank - * and are now waiting for the user to confirm the withdraw - * with the bank (typically 2nd factor auth). - */ - PendingWaitConfirmBank = 0x0100_0002, - SuspendedWaitConfirmBank = 0x0110_0002, - - /** - * Querying reserve status with the exchange. - */ - PendingQueryingStatus = 0x0100_0003, - SuspendedQueryingStatus = 0x0110_0003, - - /** - * Ready for withdrawal. - */ - PendingReady = 0x0100_0004, - SuspendedReady = 0x0110_0004, - - /** - * Redenominate the withdrawal - * after the exchange entry is ready again. - */ - PendingRedenominate = 0x0100_0008, - SuspendedRedenominate = 0x0110_0008, - - /** - * Exchange wants KYC info from the user. - */ - PendingKyc = 0x0100_0005, - SuspendedKyc = 0x0110_005, - - /** - * Exchange wants KYC info from the user. - * KYC link is ready. - */ - PendingBalanceKyc = 0x0100_0006, - SuspendedBalanceKyc = 0x0110_006, - - /** - * Exchange wants KYC info from the user. - * - * KYC link is not ready yet, the KYC process is still initializing. - */ - PendingBalanceKycInit = 0x0100_0007, - SuspendedBalanceKycInit = 0x0110_007, - - /** - * Proposed to the user, has can choose to accept/refuse. - */ - DialogProposed = 0x0101_0000, - - /** - * We are telling the bank that we don't want to complete - * the withdrawal! - */ - AbortingBank = 0x0103_0001, - SuspendedAbortingBank = 0x0113_0001, - - /** - * The corresponding withdraw record has been created. - * No further processing is done, unless explicitly requested - * by the user. - */ - Done = 0x0500_0000, - - /** - * The bank aborted the withdrawal. - */ - FailedBankAborted = 0x0501_0001, - - FailedAbortingBank = 0x0501_0002, - - /** - * Aborted in a state where we were supposed to - * talk to the exchange. Money might have been - * wired or not. - */ - AbortedExchange = 0x0503_0001, - - AbortedBank = 0x0503_0002, - - /** - * User didn't refused the withdrawal. - */ - AbortedUserRefused = 0x0503_0003, - - /** - * Another wallet confirmed the withdrawal - * (by POSTing the reserve pub to the bank) - * before we had the chance. - * - * In this situation, we'll let the other wallet continue - * and give up ourselves. - */ - AbortedOtherWallet = 0x0503_0004, -} - -/** - * Extra info about a withdrawal that is used - * with a bank-integrated withdrawal. - */ -export interface ReserveBankInfo { - talerWithdrawUri: string; - - /** - * URL that the user can be redirected to, and allows - * them to confirm (or abort) the bank-integrated withdrawal. - */ - confirmUrl: string | undefined; - - /** - * Exchange payto URI that the bank will use to fund the reserve. - */ - exchangePaytoUri?: string; - - /** - * Time when the information about this reserve was posted to the bank. - * - * Only applies if bankWithdrawStatusUrl is defined. - * - * Set to undefined if that hasn't happened yet. - */ - timestampReserveInfoPosted: DbPreciseTimestamp | undefined; - - /** - * Time when the reserve was confirmed by the bank. - * - * Set to undefined if not confirmed yet. - */ - timestampBankConfirmed: DbPreciseTimestamp | undefined; - - wireTypes: string[] | undefined; - - currency: string | undefined; - - externalConfirmation?: boolean; - - senderWire?: string; -} - -/** - * Status of a denomination. - */ -export enum DenominationVerificationStatus { - /** - * Verification was delayed (pending). - */ - Unverified = 0x0100_0000, - - /** - * Verified as valid. - */ - VerifiedGood = 0x0500_0000, - - /** - * Verified as invalid. - */ - VerifiedBad = 0x0501_0000, -} - -export interface DenomFees { - /** - * Fee for withdrawing. - */ - feeWithdraw: AmountString; - - /** - * Fee for depositing. - */ - feeDeposit: AmountString; - - /** - * Fee for refreshing. - */ - feeRefresh: AmountString; - - /** - * Fee for refunding. - */ - feeRefund: AmountString; -} - -export interface DenominationFamilyRecord { - denominationFamilySerial?: number; - familyParams: DenomFamilyParams; - - // Reserved legacy fields: - // * familyParamsHash -} - -/** - * Denomination record as stored in the wallet's database. - */ -export interface DenominationRecord { - /** - * Currency of the denomination. - * - * Stored separately as we have an index on it. - */ - currency: string; - - value: AmountString; - - /** - * The denomination public key. - */ - denomPub: DenominationPubKey; - - /** - * Hash of the denomination public key. - * Stored in the database for faster lookups. - */ - denomPubHash: string; - - fees: DenomFees; - - denominationFamilySerial: number; - - /** - * Validity start date of the denomination. - */ - stampStart: DbProtocolTimestamp; - - /** - * Date after which the currency can't be withdrawn anymore. - */ - stampExpireWithdraw: DbProtocolTimestamp; - - /** - * Date after the denomination officially doesn't exist anymore. - */ - stampExpireLegal: DbProtocolTimestamp; - - /** - * Data after which coins of this denomination can't be deposited anymore. - */ - stampExpireDeposit: DbProtocolTimestamp; - - /** - * Signature by the exchange's master key over the denomination - * information. - */ - masterSig: string; - - /** - * Did we verify the signature on the denomination? - */ - verificationStatus: DenominationVerificationStatus; - - /** - * Was this denomination still offered by the exchange the last time - * we checked? - * Only false when the exchange redacts a previously published denomination. - */ - isOffered: boolean; - - /** - * Did the exchange revoke the denomination? - * When this field is set to true in the database, the same transaction - * should also mark all affected coins as revoked. - */ - isRevoked: boolean; - - /** - * If set to true, the exchange announced that the private key for this - * denomination is lost. Thus it can't be used to sign new coins - * during withdrawal/refresh/..., but the coins can still be spent. - */ - isLost?: boolean; - - /** - * Base URL of the exchange. - */ - exchangeBaseUrl: string; - - /** - * Master public key of the exchange that made the signature - * on the denomination. - */ - exchangeMasterPub: string; -} - -export namespace DenominationRecord { - export function toDenomInfo(d: DenominationRecord): DenominationInfo { - return { - denomPub: d.denomPub, - exchangeMasterPub: d.exchangeMasterPub, - denomPubHash: d.denomPubHash, - feeDeposit: Amounts.stringify(d.fees.feeDeposit), - feeRefresh: Amounts.stringify(d.fees.feeRefresh), - feeRefund: Amounts.stringify(d.fees.feeRefund), - feeWithdraw: Amounts.stringify(d.fees.feeWithdraw), - stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit), - stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal), - stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw), - stampStart: timestampProtocolFromDb(d.stampStart), - value: Amounts.stringify(d.value), - exchangeBaseUrl: d.exchangeBaseUrl, - isLost: d.isLost ?? false, - masterSig: d.masterSig, - isOffered: d.isOffered, - }; - } -} - -export interface ExchangeSignkeysRecord { - stampStart: DbProtocolTimestamp; - stampExpire: DbProtocolTimestamp; - stampEnd: DbProtocolTimestamp; - signkeyPub: EddsaPublicKeyString; - masterSig: EddsaSignatureString; - - /** - * Exchange details that thiis signkeys record belongs to. - */ - exchangeDetailsRowId: number; -} - -/** - * Exchange details for a particular - * (exchangeBaseUrl, masterPublicKey, currency) tuple. - */ -export interface ExchangeDetailsRecord { - rowId?: number; - - /** - * Master public key of the exchange. - */ - masterPublicKey: string; - - exchangeBaseUrl: string; - - /** - * Currency that the exchange offers. - */ - currency: string; - - /** - * Auditors (partially) auditing the exchange. - */ - auditors: ExchangeAuditor[]; - - /** - * Last observed protocol version. - */ - protocolVersionRange: string; - - tinyAmount: AmountString; - - reserveClosingDelay: TalerProtocolDuration; - - shoppingUrl?: string; - - /** - * Fees for exchange services - */ - globalFees: ExchangeGlobalFees[]; - - wireInfo: WireInfo; - - /** - * Age restrictions supported by the exchange (bitmask). - */ - ageMask?: number; - - walletBalanceLimits?: AmountString[]; - - hardLimits?: AccountLimit[]; - - zeroLimits?: ZeroLimitedOperation[]; - - /** - * Instructs wallets to use certain bank-specific - * language (for buttons) and/or other UI/UX customization - * for compliance with the rules of that bank. - */ - bankComplianceLanguage: string | undefined; - - defaultPeerPushExpiration: TalerProtocolDuration | undefined; -} - -export interface ExchangeDetailsPointer { - masterPublicKey: string; - - currency: string; - - /** - * Timestamp when the (masterPublicKey, currency) pointer - * has been updated. - */ - updateClock: DbPreciseTimestamp; -} - -export enum ExchangeEntryDbRecordStatus { - Preset = 1, - Ephemeral = 2, - Used = 3, -} - -// FIXME: Use status ranges for this as well? -export enum ExchangeEntryDbUpdateStatus { - Initial = 1, - InitialUpdate = 2, - Suspended = 3, - UnavailableUpdate = 4, - // Reserved 5 for backwards compatibility. - Ready = 6, - ReadyUpdate = 7, - OutdatedUpdate = 8, -} - -/** - * Exchange record as stored in the wallet's database. - */ -export interface ExchangeEntryRecord { - /** - * Base url of the exchange. - */ - baseUrl: string; - - /** - * Currency hint for a preset exchange, relevant - * when we didn't contact a preset exchange yet. - */ - presetCurrencyHint?: string; - - /** - * Currency spec for a preset exchange, relevant - * when we didn't contact a preset exchange yet. - */ - presetCurrencySpec?: CurrencySpecification; - - /** - * Type of the exchange, if it was a preset entry. - */ - presetType?: string; - - /** - * When did we confirm the last withdrawal from this exchange? - * - * Used mostly in the UI to suggest exchanges. - */ - lastWithdrawal?: DbPreciseTimestamp; - - /** - * Pointer to the current exchange details. - * - * Should usually not change. Only changes when the - * exchange advertises a different master public key and/or - * currency. - * - * We could use a rowID here, but having the currency in the - * details pointer lets us do fewer DB queries - */ - detailsPointer: ExchangeDetailsPointer | undefined; - - entryStatus: ExchangeEntryDbRecordStatus; - - updateStatus: ExchangeEntryDbUpdateStatus; - - unavailableReason?: TalerErrorDetail; - - /** - * If set to true, the next update to the exchange - * status will request /keys with no-cache headers set. - */ - cachebreakNextUpdate?: boolean; - - /** - * Etag of the current ToS of the exchange. - */ - tosCurrentEtag: string | undefined; - - tosAcceptedEtag: string | undefined; - - tosAcceptedTimestamp: DbPreciseTimestamp | undefined; - - /** - * Last time when the exchange /keys info was updated - * successfully. - */ - lastUpdate: DbPreciseTimestamp | undefined; - - /** - * Next scheduled update for the exchange. - */ - nextUpdateStamp: DbPreciseTimestamp; - - lastKeysEtag: string | undefined; - - /** - * Next time that we should check if coins need to be refreshed. - * - * Updated whenever the exchange's denominations are updated or when - * the refresh check has been done. - */ - nextRefreshCheckStamp: DbPreciseTimestamp; - - /** - * Public key of the reserve that we're currently using for - * receiving P2P payments. - */ - currentMergeReserveRowId?: number; - - /** - * Current account private key. The corresponding public - * key is used as the merchant public key in deposits. - * - * When unset or reset, we use a heuristic to find an - * account priv/pub that likely already has KYC auth. - */ - currentAccountPriv?: string; - - /** - * @see currentAccountPriv - */ - currentAccountPub?: string; - - /** - * Defaults to false. - */ - peerPaymentsDisabled?: boolean; - - /** - * Are direct deposits using this exchange disabled? - * Defaults to false. - */ - directDepositDisabled?: boolean; - - /** - * Defaults to false. - */ - noFees?: boolean; -} - -export enum PlanchetStatus { - Pending = 0x0100_0000, - KycRequired = 0x0100_0001, - WithdrawalDone = 0x0500_000, - AbortedReplaced = 0x0503_0001, -} - -/** - * A coin that isn't yet signed by an exchange. - */ -export interface PlanchetRecord { - /** - * Public key of the coin. - */ - coinPub: string; - - /** - * Private key of the coin. - */ - coinPriv: string; - - /** - * Withdrawal group that this planchet belongs to - * (or the empty string). - */ - withdrawalGroupId: string; - - /** - * Index within the withdrawal group (or -1). - */ - coinIdx: number; - - planchetStatus: PlanchetStatus; - - lastError: TalerErrorDetail | undefined; - - denomPubHash: string; - - blindingKey: string; - - withdrawSig: string; - - coinEv: CoinEnvelope; - - coinEvHash: string; - - ageCommitmentProof?: AgeCommitmentProof; -} - -export enum CoinSourceType { - Withdraw = "withdraw", - Refresh = "refresh", - Reward = "reward", -} - -export interface WithdrawCoinSource { - type: CoinSourceType.Withdraw; - - /** - * Can be the empty string for orphaned coins. - */ - withdrawalGroupId: string; - - /** - * Index of the coin in the withdrawal session. - */ - coinIndex: number; - - /** - * Reserve public key for the reserve we got this coin from. - */ - reservePub: string; -} - -export interface RefreshCoinSource { - type: CoinSourceType.Refresh; - refreshGroupId: string; - oldCoinPub: string; -} - -export interface RewardCoinSource { - type: CoinSourceType.Reward; - walletRewardId: string; - coinIndex: number; -} - -export type CoinSource = - | WithdrawCoinSource - | RefreshCoinSource - | RewardCoinSource; - -/** - * CoinRecord as stored in the "coins" data store - * of the wallet database. - */ -export interface CoinRecord { - /** - * Where did the coin come from? Used for recouping coins. - */ - coinSource: CoinSource; - - /** - * Source transaction ID of the coin. - * - * Used to make the coin visible after the transaction - * has entered a final state. - */ - sourceTransactionId?: string; - - /** - * Public key of the coin. - */ - coinPub: string; - - /** - * Private key to authorize operations on the coin. - */ - coinPriv: string; - - /** - * Hash of the public key that signs the coin. - */ - denomPubHash: string; - - /** - * Unblinded signature by the exchange. - */ - denomSig: UnblindedDenominationSignature; - - /** - * Base URL that identifies the exchange from which we got the - * coin. - */ - exchangeBaseUrl: string; - - /** - * Blinding key used when withdrawing the coin. - * Potentionally used again during payback. - */ - blindingKey: string; - - /** - * Hash of the coin envelope. - * - * Stored here for indexing purposes, so that when looking at a - * reserve history, we can quickly find the coin for a withdrawal transaction. - */ - coinEvHash: string; - - /** - * Status of the coin. - */ - status: CoinStatus; - - /** - * Non-zero for visible. - * - * A coin is visible when it is fresh and the - * source transaction is in a final state. - */ - visible?: number; - - /** - * Maximum age of purchases that can be made with this coin. - * - * (Used for indexing, redundant with {@link ageCommitmentProof}). - */ - maxAge: number; - - ageCommitmentProof: AgeCommitmentProof | undefined; -} - -/** - * Object to be hashed for use as a grouping key for token listings, such that - * any change in token family details results in a separate list item. - */ -export interface TokenFamilyInfo { - /** - * Identifier for the token family consisting of - * unreserved characters according to RFC 3986. - */ - slug: string; - - /** - * Human-readable name for the token family. - */ - name: string; - - /** - * Human-readable description for the token family. - */ - description: string; - - /** - * Optional map from IETF BCP 47 language tags to localized descriptions. - */ - descriptionI18n: any | undefined; - - /** - * Additional meta data, such as the trusted_domains - * or expected_domains. Depends on the kind. - */ - extraData: MerchantContractTokenDetails; - - /** - * Token issue public key used by merchant to verify tokens. - */ - tokenIssuePub: TokenIssuePublicKey; -} - -/** - * TokenFamilyRecord as stored in the "tokenFamilies" - * data store of the wallet database. - */ -export interface TokenRecord extends TokenFamilyInfo { - /** - * Source purchase of the token. - */ - purchaseId: string; - - /** - * Transaction where token is being used. - */ - transactionId?: string; - - /** - * Index of token in choices array. - */ - choiceIndex?: number; - - /** - * Index of token in outputs array. - */ - outputIndex?: number; - - /** - * For token outputs with a count>1, this stores - * the index of this token within the same output - * index. - * - * If missing, assumed to be 0. - */ - repeatIndex?: number; - - /** - * URL of the merchant issuing the token. - */ - merchantBaseUrl: string; - - /** - * Kind of the token. - */ - kind: MerchantContractTokenKind; - - /** - * Hash of token issue public key. - */ - tokenIssuePubHash: string; - - /** - * Hash of {@link TokenFamilyInfo} object. - */ - tokenFamilyHash?: string; - - /** - * Start time of the token family's validity period. - */ - validAfter: DbProtocolTimestamp; - - /** - * End time of the token family's validity period. - */ - validBefore: DbProtocolTimestamp; - - /** - * Unblinded token issue signature made by the merchant. - */ - tokenIssueSig: UnblindedDenominationSignature; - - /** - * Token use public key used to confirm usage of tokens. - */ - tokenUsePub: string; - - /** - * Token use private key used to verify usage of tokens. - */ - tokenUsePriv: string; - - /** - * Signature on token use request. - */ - tokenUseSig?: TokenUseSig; - - /** - * Envelope of the token. - */ - tokenEv: TokenEnvelope; - - /** - * Hash of the envelope. - */ - tokenEvHash: string; - - /** - * Blinding secret for token. - */ - blindingKey: string; -} - -/** - * Slate, a blank slice of rock cut for use as a writing surface, - * also the database representation of a token before being - * signed by the merchant, as stored in the `slates' data store. - */ -export type SlateRecord = Omit<TokenRecord, "tokenIssueSig">; - -export namespace TokenRecord { - export function hashInfo(r: TokenRecord | SlateRecord): string { - const info: TokenFamilyInfo = { - slug: r.slug, - name: r.name, - description: r.description, - descriptionI18n: r.descriptionI18n, - extraData: r.extraData, - tokenIssuePub: r.tokenIssuePub, - }; - return encodeCrock(hash(stringToBytes(canonicalJson(info) + "\0"))); - } -} - -/** - * History item for a coin. - * - * DB-specific format, - */ -export type DbWalletCoinHistoryItem = - | { - type: "withdraw"; - transactionId: TransactionIdStr; - } - | { - type: "spend"; - transactionId: TransactionIdStr; - amount: AmountString; - } - | { - type: "refresh"; - transactionId: TransactionIdStr; - amount: AmountString; - } - | { - type: "recoup"; - transactionId: TransactionIdStr; - amount: AmountString; - } - | { - type: "refund"; - transactionId: TransactionIdStr; - amount: AmountString; - }; - -/** - * History event for a coin from the wallet's perspective. - * - * The history might reference transactions that were already deleted from the wallet. - */ -export interface CoinHistoryRecord { - coinPub: string; - /** - * History items for the coin. - * - * We store this as an array in the object store, as the coin history - * is pretty much always very small. - */ - history: DbWalletCoinHistoryItem[]; -} - -export enum RefreshCoinStatus { - Pending = 0x0100_0000, - - /** - * Re-try the melt with a new target denomination. - */ - PendingRedenominate = 0x0100_0001, - - Finished = 0x0500_0000, - - /** - * The refresh for this coin has been frozen, because of a permanent error. - * More info in lastErrorPerCoin. - */ - Failed = 0x0501_000, -} - -export enum RefreshOperationStatus { - Pending = 0x0100_0000, - /** - * Entire output coin selection was bad, re-select - * and potentially revive finished coins with zero output. - */ - PendingRedenominate = 0x0100_0001, - Suspended = 0x0110_0000, - SuspendedRedenominate = 0x0110_0001, - - Finished = 0x0500_000, - Failed = 0x0501_000, -} - -/** - * Status of a single element of a deposit group. - */ -export enum DepositElementStatus { - DepositPending = 0x0100_0000, - /** - * Accepted, but tracking. - */ - Tracking = 0x0100_0001, - KycRequired = 0x0100_0002, - Wired = 0x0500_0000, - RefundSuccess = 0x0503_0000, - RefundFailed = 0x0501_0000, - RefundNotFound = 0x0501_0001, -} - -export interface RefreshGroupPerExchangeInfo { - /** - * (Expected) output once the refresh group succeeded. - */ - outputEffective: AmountString; -} - -/** - * Group of refresh operations. The refreshed coins do not - * have to belong to the same exchange, but must have the same - * currency. - */ -export interface RefreshGroupRecord { - operationStatus: RefreshOperationStatus; - - /** - * Unique, randomly generated identifier for this group of - * refresh operations. - */ - refreshGroupId: string; - - /** - * Currency of this refresh group. - */ - currency: string; - - /** - * Reason why this refresh group has been created. - */ - reason: RefreshReason; - - originatingTransactionId?: string; - - oldCoinPubs: string[]; - - inputPerCoin: AmountString[]; - - expectedOutputPerCoin: AmountString[]; - - infoPerExchange?: Record<string, RefreshGroupPerExchangeInfo>; - - /** - * Flag for each coin whether refreshing finished. - * If a coin can't be refreshed (remaining value too small), - * it will be marked as finished, but no refresh session will - * be created. - */ - statusPerCoin: RefreshCoinStatus[]; - - /** - * Refund requests that might still be necessary - * before the refresh can work. - */ - refundRequests: { [n: number]: ExchangeRefundRequest }; - - timestampCreated: DbPreciseTimestamp; - - failReason?: TalerErrorDetail; - - /** - * Timestamp when the refresh session finished. - */ - timestampFinished: DbPreciseTimestamp | undefined; -} - -/** - * Ongoing refresh - */ -export interface RefreshSessionRecord { - refreshGroupId: string; - - /** - * Index of the coin in the refresh group. - */ - coinIndex: number; - - /** - * If this field is set, it's a V2 refresh session. - */ - sessionPublicSeed?: string; - - /** - * Sum of the value of denominations we want - * to withdraw in this session, without fees. - */ - amountRefreshOutput: AmountString; - - /** - * Hashed denominations of the newly requested coins. - */ - newDenoms: { - denomPubHash: string; - count: number; - }[]; - - /** - * The no-reveal-index after we've done the melting. - */ - norevealIndex?: number; - - /** - * Last error response from the exchange. - * - * FIXME: We don't store the last HTTP status yet. - */ - lastError?: TalerErrorDetail; - - // Reserved legacy fields: - // * sessionSecretSeed: string - // (legacy v1 refresh) -} - -export enum RefundReason { - /** - * Normal refund given by the merchant. - */ - NormalRefund = "normal-refund", - /** - * Refund from an aborted payment. - */ - AbortRefund = "abort-pay-refund", -} - -export enum PurchaseStatus { - /** - * Not downloaded yet. - */ - PendingDownloadingProposal = 0x0100_0000, - SuspendedDownloadingProposal = 0x0110_0000, - - /** - * The user has accepted the proposal. - */ - PendingPaying = 0x0100_0001, - SuspendedPaying = 0x0110_0001, - - /** - * Currently in the process of aborting with a refund. - */ - AbortingWithRefund = 0x0103_0000, - SuspendedAbortingWithRefund = 0x0113_0000, - - /** - * Paying a second time, likely with different session ID - */ - PendingPayingReplay = 0x0100_0002, - SuspendedPayingReplay = 0x0110_0002, - - /** - * Query for refunds (until query succeeds). - */ - PendingQueryingRefund = 0x0100_0003, - SuspendedQueryingRefund = 0x0110_0003, - - /** - * Query for refund (until auto-refund deadline is reached). - * - * Legacy state for compatibility. - */ - PendingQueryingAutoRefund = 0x0100_0004, - SuspendedQueryingAutoRefund = 0x0110_0004, - - FinalizingQueryingAutoRefund = 0x0200_0001, - SuspendedFinalizingQueryingAutoRefund = 0x0210_0001, - - PendingAcceptRefund = 0x0100_0005, - SuspendedPendingAcceptRefund = 0x0110_0005, - - /** - * Proposal downloaded, but the user needs to accept/reject it. - */ - DialogProposed = 0x0101_0000, - - /** - * Proposal shared to other wallet or read from other wallet - * the user needs to accept/reject it. - */ - DialogShared = 0x0101_0001, - - /** - * Generic failure, check error code. - */ - Failed = 0x0501_0000, - - /** - * Tried to abort, but aborting failed or was cancelled. - */ - FailedAbort = 0x0501_0001, - - FailedPaidByOther = 0x0501_0002, - - /** - * Downloading or processing the proposal has failed permanently. - */ - FailedClaim = 0x0501_0003, - - /** - * Payment was successful. - */ - Done = 0x0500_0000, - - /** - * Downloaded proposal was detected as a re-purchase. - */ - DoneRepurchaseDetected = 0x0500_0001, - - Expired = 0x0502_0000, - - /** - * The user has rejected the proposal. - */ - AbortedProposalRefused = 0x0503_0000, - - AbortedRefunded = 0x0503_0001, - - AbortedOrderDeleted = 0x0503_0002, - - /** - * The payment has been aborted. - */ - AbortedIncompletePayment = 0x0503_0003, -} - -/** - * Partial information about the downloaded proposal. - * Only contains data that is relevant for indexing on the - * "purchases" object stores. - */ -export interface ProposalDownloadInfo { - contractTermsHash: string; - fulfillmentUrl?: string; - currency: string; - contractTermsMerchantSig: string; -} - -export interface DbCoinSelection { - coinPubs: string[]; - coinContributions: AmountString[]; -} - -export interface DbTokenSelection { - tokenPubs: string[]; -} - -export interface PurchasePayInfo { - /** - * Undefined if payment is blocked by a pending refund. - */ - payCoinSelection?: DbCoinSelection; - /** - * Undefined if payment is blocked by a pending refund. - */ - payCoinSelectionUid?: string; - - payTokenSelection?: DbTokenSelection; - - /** - * Token signatures from merchant. - */ - slateTokenSigs?: SignedTokenEnvelope[]; - - /** - * Whether token selection should be forced - * e.g. when merchant URL is not in `expected_domains' - */ - payTokenForcedSel?: boolean; - - totalPayCost: AmountString; -} - -/** - * Record that stores status information about one purchase, starting from when - * the customer accepts a proposal. Includes refund status if applicable. - * - * Key: {@link proposalId} - * Operation status: {@link purchaseStatus} - */ -export interface PurchaseRecord { - /** - * Proposal ID for this purchase. Uniquely identifies the - * purchase and the proposal. - * Assigned by the wallet. - */ - proposalId: string; - - /** - * Order ID, assigned by the merchant. - */ - orderId: string; - - merchantBaseUrl: string; - - /** - * Claim token used when downloading the contract terms. - */ - claimToken: string | undefined; - - /** - * Session ID we got when downloading the contract. - */ - downloadSessionId: string | undefined; - - /** - * If this purchase is a repurchase, this field identifies the original purchase. - */ - repurchaseProposalId: string | undefined; - - purchaseStatus: PurchaseStatus; - - /** - * Refresh group ID of the refresh transaction that - * has been created to abort the payment. - */ - abortRefreshGroupId?: string; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; - - /** - * Private key for the nonce. - */ - noncePriv: string; - - /** - * Public key for the nonce. - */ - noncePub: string; - - /** - * Index of selected choice in the choices array. - */ - choiceIndex?: number | undefined; - - /** - * Secret seed used to derive slates. - * Stored since slates are created lazily. - */ - secretSeed: string | undefined; - - /** - * Downloaded and parsed proposal data. - */ - download: ProposalDownloadInfo | undefined; - - payInfo: PurchasePayInfo | undefined; - - /** - * Exchanges involved in this purchase. - * Used as a multiEntry index to find all purchases for - * an exchange. - */ - exchanges?: string[]; - - /** - * Pending removals from pay coin selection. - * - * Used when a the pay coin selection needs to be changed - * because a coin became known as double-spent or invalid, - * but a new coin selection can't immediately be done, as - * there is not enough balance (e.g. when waiting for a refresh). - */ - pendingRemovedCoinPubs?: string[]; - - /** - * Timestamp of the first time that sending a payment to the merchant - * for this purchase was successful. - */ - timestampFirstSuccessfulPay: DbPreciseTimestamp | undefined; - - merchantPaySig: string | undefined; - - posConfirmation: string | undefined; - - donauOutputIndex?: number; - donauBaseUrl?: string; - donauAmount?: AmountString; - donauTaxIdHash?: string; - donauTaxIdSalt?: string; - donauTaxId?: string; - donauYear?: number; - - /** - * This purchase was shared with another wallet - * that is now supposed to finish the payment. - */ - shared: boolean; - - /** - * This purchase was created by reading - * a payment share or the wallet - * the nonce public by a payment share - * - * Defaults to false. - */ - createdFromShared?: boolean; - - /** - * When was the purchase record created? - */ - timestamp: DbPreciseTimestamp; - - /** - * When was the purchase made? - * Refers to the time that the user accepted. - */ - timestampAccept: DbPreciseTimestamp | undefined; - - /** - * When was the last refund made? - * Set to 0 if no refund was made on the purchase. - */ - timestampLastRefundStatus: DbPreciseTimestamp | undefined; - - /** - * Timestamp when the wallet noticed that the transaction expired. - * May be later than the pay deadline. - */ - timestampExpired?: DbPreciseTimestamp; - - /** - * Last session signature that we submitted to /pay (if any). - */ - lastSessionId: string | undefined; - - /** - * Continue querying the refund status until this deadline has expired. - */ - autoRefundDeadline: DbProtocolTimestamp | undefined; - - /** - * How much merchant has refund to be taken but the wallet - * did not picked up yet - */ - refundAmountAwaiting: AmountString | undefined; -} - -export enum ConfigRecordKey { - WalletBackupState = "walletBackupState", - CurrencyDefaultsApplied = "currencyDefaultsApplied", - // Only for testing, do not use! - TestLoopTx = "testTxLoop", - LastInitInfo = "lastInitInfo", - MaterializedTransactionsVersion = "materializedTransactionsVersion", - DonauConfig = "donauConfig", -} - -export interface DonauConfig { - donauBaseUrl: string; - donauTaxId: string; - /** Tax ID hash, salted with donauSalt */ - donauTaxIdHash: string; - /** 32 byte salt, base32crockford encoded */ - donauSalt: string; -} - -/** - * Configuration key/value entries to configure - * the wallet. - */ -export type ConfigRecord = - | { - key: ConfigRecordKey.WalletBackupState; - value: WalletBackupConfState; - } - | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean | number } - | { key: ConfigRecordKey.TestLoopTx; value: number } - | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp } - | { key: ConfigRecordKey.MaterializedTransactionsVersion; value: number } - | { key: ConfigRecordKey.DonauConfig; value: DonauConfig }; - -export interface WalletBackupConfState { - deviceId: string; - walletRootPub: string; - walletRootPriv: string; - - /** - * Last hash of the canonicalized plain-text backup. - */ - lastBackupPlainHash?: string; - - /** - * Timestamp stored in the last backup. - */ - lastBackupTimestamp?: DbPreciseTimestamp; - - /** - * Last time we tried to do a backup. - */ - lastBackupCheckTimestamp?: DbPreciseTimestamp; - lastBackupNonce?: string; -} - -// FIXME: Should these be numeric codes? -export const enum WithdrawalRecordType { - BankManual = "bank-manual", - BankIntegrated = "bank-integrated", - PeerPullCredit = "peer-pull-credit", - PeerPushCredit = "peer-push-credit", - Recoup = "recoup", -} - -export interface WgInfoBankIntegrated { - withdrawalType: WithdrawalRecordType.BankIntegrated; - - /** - * Extra state for when this is a withdrawal involving - * a Taler-integrated bank. - */ - bankInfo: ReserveBankInfo; - - /** - * Info about withdrawal accounts, possibly including currency conversion. - */ - exchangeCreditAccounts?: WithdrawalExchangeAccountDetails[]; -} - -export interface WgInfoBankManual { - withdrawalType: WithdrawalRecordType.BankManual; - - /** - * Info about withdrawal accounts, possibly including currency conversion. - */ - exchangeCreditAccounts?: WithdrawalExchangeAccountDetails[]; -} - -export interface WgInfoBankPeerPull { - withdrawalType: WithdrawalRecordType.PeerPullCredit; - - // FIXME: include a transaction ID here? - - /** - * Needed to quickly construct the taler:// URI for the counterparty - * without a join. - */ - contractPriv: string; -} - -export interface WgInfoBankPeerPush { - withdrawalType: WithdrawalRecordType.PeerPushCredit; - - // FIXME: include a transaction ID here? -} - -export interface WgInfoBankRecoup { - withdrawalType: WithdrawalRecordType.Recoup; -} - -export type WgInfo = - | WgInfoBankIntegrated - | WgInfoBankManual - | WgInfoBankPeerPull - | WgInfoBankPeerPush - | WgInfoBankRecoup; - -export type KycUserType = "individual" | "business"; - -/** - * Group of withdrawal operations that need to be executed. - * (Either for a normal withdrawal or from a reward.) - * - * The withdrawal group record is only created after we know - * the coin selection we want to withdraw. - */ -export interface WithdrawalGroupRecord { - /** - * Unique identifier for the withdrawal group. - */ - withdrawalGroupId: string; - - wgInfo: WgInfo; - - /** - * If set to true, the account used during withdrawal is treated as an - * account that does not belong to the user. It won't be shown in - * the list of know bank accounts. - * - * Defaults to false. - */ - isForeignAccount?: boolean; - - kycPaytoHash?: string; - - kycAccessToken?: string; - - kycLastCheckStatus?: number | undefined; - kycLastCheckCode?: number | undefined; - kycLastRuleGen?: number | undefined; - kycLastAmlReview?: boolean | undefined; - kycLastDeny?: DbPreciseTimestamp | undefined; - - /** - * Delay to wait until the next withdrawal attempt. - * - * @deprecated by https://bugs.gnunet.org/view.php?id=9694 - */ - kycWithdrawalDelay?: TalerProtocolDuration; - - /** - * Secret seed used to derive planchets. - * Stored since planchets are created lazily. - */ - secretSeed: string; - - /** - * Public key of the reserve that we're withdrawing from. - */ - reservePub: string; - - /** - * The reserve private key. - * - * FIXME: Already in the reserves object store, redundant! - */ - reservePriv: string; - - /** - * The exchange base URL that we're withdrawing from. - * (Redundantly stored, as the reserve record also has this info.) - */ - exchangeBaseUrl?: string; - - /** - * When was the withdrawal operation started started? - * Timestamp in milliseconds. - */ - timestampStart: DbPreciseTimestamp; - - /** - * When was the withdrawal operation completed? - */ - timestampFinish?: DbPreciseTimestamp; - - /** - * Current status of the reserve. - */ - status: WithdrawalGroupStatus; - - /** - * Restrict withdrawals from this reserve to this age. - */ - restrictAge?: number; - - /** - * Amount that was sent by the user to fund the reserve. - */ - instructedAmount?: AmountString; - - /** - * Amount that was observed when querying the reserve that - * we are withdrawing from. - * - * Useful for diagnostics. - */ - reserveBalanceAmount?: AmountString; - - /** - * Amount including fees (i.e. the amount subtracted from the - * reserve to withdraw all coins in this withdrawal session). - * - * (Initial amount confirmed by the user, might differ with denomSel - * on reselection.) - */ - rawWithdrawalAmount?: AmountString; - - /** - * Amount that will be added to the balance when the withdrawal succeeds. - * - * (Initial amount confirmed by the user, might differ with denomSel - * on reselection.) - */ - effectiveWithdrawalAmount?: AmountString; - - /** - * Denominations selected for withdrawal. - */ - denomsSel?: DenomSelectionState; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; -} - -export interface BankWithdrawUriRecord { - /** - * The withdraw URI we got from the bank. - */ - talerWithdrawUri: string; - - /** - * Reserve that was created for the withdraw URI. - */ - reservePub: string; -} - -export enum RecoupOperationStatus { - Pending = 0x0100_0000, - Suspended = 0x0110_0000, - - Finished = 0x0500_000, - Failed = 0x0501_000, -} - -/** - * Status of recoup operations that were grouped together. - * - * The remaining amount of involved coins should be set to zero - * in the same transaction that inserts the RecoupGroupRecord. - */ -export interface RecoupGroupRecord { - /** - * Unique identifier for the recoup group record. - */ - recoupGroupId: string; - - exchangeBaseUrl: string; - - operationStatus: RecoupOperationStatus; - - timestampStarted: DbPreciseTimestamp; - - timestampFinished: DbPreciseTimestamp | undefined; - - /** - * Public keys that identify the coins being recouped - * as part of this session. - * - * (Structured like this to enable multiEntry indexing in IndexedDB.) - */ - coinPubs: string[]; - - /** - * Array of flags to indicate whether the recoup finished on each individual coin. - */ - recoupFinishedPerCoin: boolean[]; - - /** - * Public keys of coins that should be scheduled for refreshing - * after all individual recoups are done. - */ - scheduleRefreshCoins: CoinRefreshRequest[]; -} - -export enum DepositOperationStatus { - PendingDeposit = 0x0100_0000, - SuspendedDeposit = 0x0110_0000, - - // Legacy states, we we now show - // the tracking state as a finalizing state. - LegacyPendingTrack = 0x0100_0001, - LegacySuspendedTrack = 0x0110_0001, - - PendingAggregateKyc = 0x0100_0002, - SuspendedAggregateKyc = 0x0110_0002, - - PendingDepositKyc = 0x0100_0003, - SuspendedDepositKyc = 0x0110_0003, - - PendingDepositKycAuth = 0x0100_0005, - SuspendedDepositKycAuth = 0x0110_0005, - - Aborting = 0x0103_0000, - SuspendedAborting = 0x0113_0000, - - FinalizingTrack = 0x0200_0001, - SuspendedFinalizingTrack = 0x0210_0001, - - Finished = 0x0500_0000, - - FailedDeposit = 0x0501_0000, - - FailedTrack = 0x0501_0001, - - AbortedDeposit = 0x0503_0000, -} - -export interface DepositTrackingInfo { - // Raw wire transfer identifier of the deposit. - wireTransferId: string; - // When was the wire transfer given to the bank. - timestampExecuted: DbProtocolTimestamp; - // Total amount transfer for this wtid (including fees) - amountRaw: AmountString; - // Wire fee amount for this exchange - wireFee: AmountString; - - exchangePub: string; -} - -export interface DepositInfoPerExchange { - /** - * Expected effective amount that will be deposited - * from coins of this exchange. - */ - amountEffective: AmountString; -} - -/** - * Group of deposits made by the wallet. - */ -export interface DepositGroupRecord { - depositGroupId: string; - - currency: string; - - /** - * Instructed amount. - */ - amount: AmountString; - - wireTransferDeadline: DbProtocolTimestamp; - - merchantPub: string; - merchantPriv: string; - - noncePriv: string; - noncePub: string; - - /** - * Wire information used by all deposits in this - * deposit group. - */ - wire: { - payto_uri: string; - salt: string; - }; - - contractTermsHash: string; - - payCoinSelection?: DbCoinSelection; - - payCoinSelectionUid?: string; - - totalPayCost: AmountString; - - /** - * The counterparty effective deposit amount. - */ - counterpartyEffectiveDepositAmount: AmountString; - - timestampCreated: DbPreciseTimestamp; - - timestampFinished: DbPreciseTimestamp | undefined; - - /** - * When did the wallet last try a deposit request? - */ - timestampLastDepositAttempt: DbPreciseTimestamp | undefined; - - operationStatus: DepositOperationStatus; - - statusPerCoin?: DepositElementStatus[]; - - infoPerExchange?: Record<string, DepositInfoPerExchange>; - - /** - * When the deposit transaction was aborted and - * refreshes were tried, we create a refresh - * group and store the ID here. - */ - abortRefreshGroupId?: string; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; - - kycInfo?: DepositKycInfo; - kycAuthTransferOptions?: TransferOptionRaw[]; - kycAuthTransferExpiry?: TalerProtocolTimestamp; - - // FIXME: Do we need this and should it be in this object store? - trackingState?: { - [signature: string]: DepositTrackingInfo; - }; -} - -export interface DepositKycInfo { - accessToken?: string; - paytoHash: string; - exchangeBaseUrl: string; - lastCheckStatus?: number | undefined; - lastCheckCode?: number | undefined; - lastRuleGen?: number | undefined; - lastAmlReview?: boolean | undefined; - lastDeny?: DbPreciseTimestamp | undefined; - lastBadKycAuth?: boolean; -} - -export interface TombstoneRecord { - /** - * Tombstone ID, with the syntax "tmb:<type>:<key>". - */ - id: string; -} - -export enum PeerPushDebitStatus { - /** - * Initiated, but no purse created yet. - */ - PendingCreatePurse = 0x0100_0000 /* ACTIVE_START */, - PendingReady = 0x0100_0001, - AbortingDeletePurse = 0x0103_0000, - - SuspendedCreatePurse = 0x0110_0000, - SuspendedReady = 0x0110_0001, - SuspendedAbortingDeletePurse = 0x0113_0000, - - Done = 0x0500_0000, - Aborted = 0x0503_0000, - Failed = 0x0501_0000, - Expired = 0x0502_0000, - - // Legacy / reserved: - // SuspendedAbortingRefreshDeleted = 0x0113_0001, - // SuspendedAbortingRefreshExpired = 0x0113_0002, - // AbortingRefreshDeleted = 0x0103_0001, - // AbortingRefreshExpired = 0x0103_0002, -} - -export interface DbPeerPushPaymentCoinSelection { - contributions: AmountString[]; - coinPubs: CoinPublicKeyString[]; -} - -/** - * Record for a push P2P payment that this wallet initiated. - */ -export interface PeerPushDebitRecord { - /** - * What exchange are funds coming from? - */ - exchangeBaseUrl: string; - - /** - * Restricted scope for this transaction. - * - * Relevant for coin reselection. - */ - restrictScope?: ScopeInfo; - - /** - * Instructed amount. - */ - amount: AmountString; - - /** - * Effective amount. - * - * (Called totalCost for historical reasons.) - */ - totalCost: AmountString; - - coinSel?: DbPeerPushPaymentCoinSelection; - - contractTermsHash: HashCodeString; - - /** - * Purse public key. Used as the primary key to look - * up this record. - */ - pursePub: string; - - /** - * Purse private key. - */ - pursePriv: string; - - /** - * Public key of the merge capability of the purse. - */ - mergePub: string; - - /** - * Private key of the merge capability of the purse. - */ - mergePriv: string; - - contractPriv: string; - contractPub: string; - - /** - * 24 byte nonce. - */ - contractEncNonce: string; - - purseExpiration: DbProtocolTimestamp; - - timestampCreated: DbPreciseTimestamp; - - abortRefreshGroupId?: string; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; - - /** - * Status of the peer push payment initiation. - */ - status: PeerPushDebitStatus; -} - -export enum PeerPullPaymentCreditStatus { - /** - * Typically the initial state of the peer-pull-credit transaction, - * purse will be created. - */ - PendingCreatePurse = 0x0100_0000, - SuspendedCreatePurse = 0x0110_0000, - - /** - * Purse created, waiting for the other party to accept the - * invoice and deposit money into it. - */ - PendingReady = 0x0100_0001, - SuspendedReady = 0x0110_0001, - - PendingMergeKycRequired = 0x0100_0002, - SuspendedMergeKycRequired = 0x0110_0002, - - PendingWithdrawing = 0x0100_0003, - SuspendedWithdrawing = 0x0110_0003, - - PendingBalanceKycRequired = 0x0100_0004, - SuspendedBalanceKycRequired = 0x0110_0004, - - PendingBalanceKycInit = 0x0100_0005, - SuspendedBalanceKycInit = 0x0110_0005, - - AbortingDeletePurse = 0x0103_0000, - SuspendedAbortingDeletePurse = 0x0113_0000, - - Done = 0x0500_0000, - Failed = 0x0501_0000, - Expired = 0x0502_0000, - Aborted = 0x0503_0000, -} - -export interface PeerPullCreditRecord { - /** - * What exchange are we using for the payment request? - */ - exchangeBaseUrl: string; - - /** - * Amount requested. - * FIXME: What type of instructed amount is i? - */ - amount: AmountString; - - estimatedAmountEffective: AmountString; - - /** - * Purse public key. Used as the primary key to look - * up this record. - */ - pursePub: string; - - /** - * Purse private key. - */ - pursePriv: string; - - /** - * Hash of the contract terms. Also - * used to look up the contract terms in the DB. - */ - contractTermsHash: string; - - mergePub: string; - mergePriv: string; - - contractPub: string; - contractPriv: string; - - contractEncNonce: string; - - mergeTimestamp: DbPreciseTimestamp; - - mergeReserveRowId: number; - - /** - * Status of the peer pull payment initiation. - */ - status: PeerPullPaymentCreditStatus; - - kycPaytoHash?: string; - - kycAccessToken?: string; - - kycLastCheckStatus?: number | undefined; - kycLastCheckCode?: number | undefined; - kycLastRuleGen?: number | undefined; - kycLastAmlReview?: boolean | undefined; - kycLastDeny?: DbPreciseTimestamp | undefined; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; - - withdrawalGroupId: string | undefined; -} - -export enum PeerPushCreditStatus { - PendingMerge = 0x0100_0000, - SuspendedMerge = 0x0110_0000, - - PendingMergeKycRequired = 0x0100_0001, - SuspendedMergeKycRequired = 0x0110_0001, - - /** - * Merge was successful and withdrawal group has been created, now - * everything is in the hand of the withdrawal group. - */ - PendingWithdrawing = 0x0100_0002, - SuspendedWithdrawing = 0x0110_0002, - - PendingBalanceKycRequired = 0x0100_0003, - SuspendedBalanceKycRequired = 0x0110_0003, - - PendingBalanceKycInit = 0x0100_0004, - SuspendedBalanceKycInit = 0x0110_0004, - - DialogProposed = 0x0101_0000, - - Done = 0x0500_0000, - Aborted = 0x0503_0000, - Failed = 0x0501_0000, - Expired = 0x0502_0000, -} - -/** - * Record for a push P2P payment that this wallet was offered. - * - * Unique: (exchangeBaseUrl, pursePub) - */ -export interface PeerPushCreditRecord { - peerPushCreditId: string; - - exchangeBaseUrl: string; - - pursePub: string; - - mergePriv: string; - - contractPriv: string; - - timestamp: DbPreciseTimestamp; - - estimatedAmountEffective: AmountString; - - /** - * Hash of the contract terms. Also - * used to look up the contract terms in the DB. - */ - contractTermsHash: string; - - /** - * Status of the peer push payment incoming initiation. - */ - status: PeerPushCreditStatus; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; - - /** - * Associated withdrawal group. - */ - withdrawalGroupId: string | undefined; - - /** - * Currency of the peer push payment credit transaction. - * - * Mandatory in current schema version, optional for compatibility - * with older (ver_minor<4) DB versions. - */ - currency: string | undefined; - - kycPaytoHash?: string; - - kycAccessToken?: string; - - kycLastCheckStatus?: number | undefined; - kycLastCheckCode?: number | undefined; - kycLastRuleGen?: number | undefined; - kycLastAmlReview?: boolean | undefined; - kycLastDeny?: DbPreciseTimestamp | undefined; -} - -export enum PeerPullDebitRecordStatus { - PendingDeposit = 0x0100_0001, - AbortingRefresh = 0x0103_0001, - - SuspendedDeposit = 0x0110_0001, - SuspendedAbortingRefresh = 0x0113_0001, - - DialogProposed = 0x0101_0001, - - Done = 0x0500_0000, - Expired = 0x0502_0000, - Aborted = 0x0503_0000, - Failed = 0x0501_0000, -} - -export interface PeerPullPaymentCoinSelection { - contributions: AmountString[]; - coinPubs: CoinPublicKeyString[]; - - /** - * Total cost based on the coin selection. - * Non undefined after status === "Accepted" - */ - totalCost: AmountString | undefined; -} - -/** - * AKA PeerPullDebit. - */ -export interface PeerPullPaymentIncomingRecord { - peerPullDebitId: string; - - pursePub: string; - - exchangeBaseUrl: string; - - amount: AmountString; - - contractTermsHash: string; - - timestampCreated: DbPreciseTimestamp; - - /** - * Contract priv that we got from the other party. - */ - contractPriv: string; - - /** - * Status of the peer push payment incoming initiation. - */ - status: PeerPullDebitRecordStatus; - - /** - * Estimated total cost when the record was created. - */ - totalCostEstimated: AmountString; - - abortRefreshGroupId?: string; - - abortReason?: TalerErrorDetail; - failReason?: TalerErrorDetail; - - coinSel?: PeerPullPaymentCoinSelection; -} - -export enum ReserveRecordStatus { - // Need to call the "/kyc-wallet" endpoint - PendingLegiInit = 0x0100_0001, - SuspendedLegiInit = 0x0110_0001, - // Need to wait for user to pass legitimization - PendingLegi = 0x0100_0002, - SuspendedLegi = 0x0110_0002, - - /** - * Done with KYC. - */ - Done = 0x0500_0000, -} - -/** - * Store for extra information about a reserve. - * - * Mostly used to store the private key for a reserve and to allow - * other records to reference the reserve key pair via a small row ID. - * - * In the future, we might also store KYC info about a reserve here. - * - * FIXME: Should reference exchange. - */ -export interface ReserveRecord { - rowId?: number; - - reservePub: string; - - reservePriv: string; - - status?: ReserveRecordStatus; - - requirementRow?: number; - - /** - * Balance threshold that we're currently requesting KYC for. - */ - thresholdRequested?: AmountString; - - /** - * Balance threshold that we already have passed KYC for. - */ - thresholdGranted?: AmountString; - - /** - * Threshold that will trigger the next KYC. - */ - thresholdNext?: AmountString; - - kycAccessToken?: string; - - amlReview?: boolean; -} - -export interface OperationRetryRecord { - /** - * Unique identifier for the operation. Typically of - * the format `${opType}-${opUniqueKey}` - * - * @see {@link TaskIdentifiers} - */ - id: string; - - lastError?: TalerErrorDetail; - - retryInfo: DbRetryInfo; -} - -/** - * Availability of coins of a given denomination (and age restriction!). - * - * We can't store this information with the denomination record, as one denomination - * can be withdrawn with multiple age restrictions. - */ -export interface CoinAvailabilityRecord { - currency: string; - value: AmountString; - denomPubHash: string; - exchangeBaseUrl: string; - exchangeMasterPub?: string; - - /** - * Age restriction on the coin, or 0 for no age restriction (or - * denomination without age restriction support). - */ - maxAge: number; - - /** - * Number of fresh coins of this denomination that are available. - */ - freshCoinCount: number; - - /** - * Number of fresh coins that are available - * and visible, i.e. the source transaction is in - * a final state. - */ - visibleCoinCount: number; - - /** - * Number of coins that we expect to obtain via a pending refresh. - */ - pendingRefreshOutputCount?: number; -} - -export interface ContractTermsRecord { - /** - * Contract terms hash. - */ - h: string; - - /** - * Contract terms JSON. - */ - contractTermsRaw: any; -} - -export interface DbExchangeHandle { - url: string; - exchangeMasterPub: string; -} - -export interface DbAuditorHandle { - url: string; - auditorPub: string; -} - -export enum RefundGroupStatus { - Pending = 0x0100_0000, - Done = 0x0500_0000, - Failed = 0x0501_0000, - Aborted = 0x0503_0000, - Expired = 0x0502_0000, -} - -/** - * Metadata about a group of refunds with the merchant. - */ -export interface RefundGroupRecord { - status: RefundGroupStatus; - - /** - * Timestamp when the refund group was created. - */ - timestampCreated: DbPreciseTimestamp; - - proposalId: string; - - refundGroupId: string; - - refreshGroupId?: string; - - amountRaw: AmountString; - - /** - * Estimated effective amount, based on - * refund fees and refresh costs. - */ - amountEffective: AmountString; -} - -export enum RefundItemStatus { - /** - * Intermittent error that the merchant is - * reporting from the exchange. - * - * We'll try again! - */ - Pending = 0x0100_0000, - /** - * Refund was obtained successfully. - */ - Done = 0x0500_0000, - /** - * Permanent error reported by the exchange - * for the refund. - */ - Failed = 0x0501_0000, -} - -/** - * Refund for a single coin in a payment with a merchant. - */ -export interface RefundItemRecord { - /** - * Auto-increment DB record ID. - */ - id?: number; - - status: RefundItemStatus; - - /** - * Mandatory since DB minor version 15. - */ - proposalId?: string; - - refundGroupId: string; - - /** - * Execution time as claimed by the merchant - */ - executionTime: DbProtocolTimestamp; - - /** - * Time when the wallet became aware of the refund. - */ - obtainedTime: DbPreciseTimestamp; - - refundAmount: AmountString; - - coinPub: string; - - rtxid: number; -} - -export function passthroughCodec<T>(): Codec<T> { - return codecForAny(); -} - -export interface GlobalCurrencyAuditorRecord { - id?: number; - currency: string; - auditorBaseUrl: string; - auditorPub: string; -} - -export interface GlobalCurrencyExchangeRecord { - id?: number; - currency: string; - exchangeBaseUrl: string; - exchangeMasterPub: string; -} - -/** - * Metadata for a transaction. - * This object store is effectively a materialzed view of transactions gathered - * from various other object stores. - * - * Primary key: transactionId - */ -export interface TransactionMetaRecord { - /** - * Transaction identifier. - * Also determines the type of the transaction. - */ - transactionId: string; - - timestamp: DbPreciseTimestamp; - - /** - * Status of the transaction, matches the status enum of the - * transaction of the type determined by the transaction ID. - */ - status: number; - - /** - * Exchanges involved in the transaction. - */ - exchanges: string[]; - - currency: string; -} - -export enum DenomLossStatus { - /** - * Done indicates that the loss happened. - */ - Done = 0x0500_0000, - - /** - * Aborted in the sense that the loss was reversed. - */ - Aborted = 0x0503_0001, -} - -export interface DenomLossEventRecord { - denomLossEventId: string; - currency: string; - denomPubHashes: string[]; - status: DenomLossStatus; - timestampCreated: DbPreciseTimestamp; - amount: string; - eventType: DenomLossEventType; - exchangeBaseUrl: string; -} - -export interface CurrencyInfoRecord { - /** - * Stringified scope info. - */ - scopeInfoStr: string; - - /** - * Currency specification. - */ - currencySpec: CurrencySpecification; - - /** - * How did the currency info get set? - */ - source: "exchange" | "user" | "preset"; -} - -export enum ExchangeMigrationReason { - MismatchedBaseUrl = "mismatched-base-url", - UnavailableOldUrl = "unavailable-old-url", -} - -export interface ExchangeMigrationLogRecord { - oldExchangeBaseUrl: string; - newExchangeBaseUrl: string; - timestamp: DbPreciseTimestamp; - /** - * Reason that triggered the exchange base URL migration. - */ - reason: ExchangeMigrationReason; -} - -export interface ExchangeBaseUrlFixupRecord { - exchangeBaseUrl: string; - replacement: string; -} - -export enum DonationReceiptStatus { - /** - * Done indicates that the receipt - * has been successfully submitted. - */ - DoneSubmitted = 0x0500_0000, - - /** - * Pending indicates that the - * receipt still needs to be submitted. - */ - Pending = 0x0100_0000, -} - -/** - * Record for donation planchets. - */ -export interface DonationPlanchetRecord { - donauBaseUrl: string; - udiNonce: HashCodeString; - donorTaxIdHash: HashCodeString; - donorHashSalt: string; - donorTaxId: string; - donationYear: number; - proposalId: string; - /** Index of this udi within the selected donation units for the purchase. */ - udiIndex: number; - blindedUdi: BlindedUniqueDonationIdentifier; - /** blinding key secret */ - bks: string; - donationUnitPubHash: HashCodeString; - value: AmountString; -} - -/** - * Record for donation receipts. - */ -export interface DonationReceiptRecord { - status: DonationReceiptStatus; - donauBaseUrl: string; - udiNonce: HashCodeString; - proposalId: string; - donationYear: number; - donationUnitPubHash: HashCodeString; - donationUnitSig: DonationReceiptSignature; - donorTaxIdHash: HashCodeString; - donorHashSalt: string; - donorTaxId: string; - value: AmountString; - /** Index of this udi within the selected donation units for the purchase. */ - udiIndex: number; -} - -export interface DonationSummaryRecord { - donauBaseUrl: string; - legalDomain?: string; - year: number; - currency: string; - amountReceiptsAvailable: AmountString; - amountReceiptsSubmitted: AmountString; -} - -/** - * Record for contacts - */ -export interface ContactRecord { - /** - * The mailbox URI of this contact - */ - mailboxBaseUri: string; - - /** - * The mailbox identity - */ - mailboxAddress: HashCodeString; - - /** - * The alias of this contact - */ - alias: string; - - /** - * The type of the alias - */ - aliasType: string; - - /** - * The source of this alias - */ - source: string; - - /** - * The local petname of this alias - */ - petname: string; -} - -/** - * Schema definition for the IndexedDB - * wallet database. - */ -export const WalletIndexedDbStoresV1 = { - exchangeBaseUrlMigrationLog: describeStoreV2({ - recordCodec: passthroughCodec<ExchangeMigrationLogRecord>(), - storeName: "exchangeBaseUrlMigrationLog", - keyPath: ["oldExchangeBaseUrl", "newExchangeBaseUrl"], - versionAdded: 18, - indexes: {}, - }), - exchangeBaseUrlFixups: describeStoreV2({ - recordCodec: passthroughCodec<ExchangeBaseUrlFixupRecord>(), - storeName: "exchangeBaseUrlFixups", - keyPath: "exchangeBaseUrl", - versionAdded: 19, - indexes: {}, - }), - denomLossEvents: describeStoreV2({ - recordCodec: passthroughCodec<DenomLossEventRecord>(), - storeName: "denomLossEvents", - keyPath: "denomLossEventId", - versionAdded: 9, - indexes: { - byCurrency: describeIndex("byCurrency", "currency", { - versionAdded: 9, - }), - byStatus: describeIndex("byStatus", "status", { - versionAdded: 10, - }), - }, - }), - transactionsMeta: describeStoreV2({ - recordCodec: passthroughCodec<TransactionMetaRecord>(), - storeName: "transactionsMeta", - keyPath: "transactionId", - versionAdded: 13, - indexes: { - byCurrency: describeIndex("byCurrency", "currency", { - versionAdded: 13, - }), - byExchange: describeIndex("byExchange", "exchanges", { - versionAdded: 13, - multiEntry: true, - }), - byTimestamp: describeIndex("byTimestamp", "timestamp", { - versionAdded: 13, - }), - byStatus: describeIndex("byStatus", "status", { - versionAdded: 13, - }), - }, - }), - currencyInfo: describeStoreV2({ - recordCodec: passthroughCodec<CurrencyInfoRecord>(), - storeName: "currencyInfo", - keyPath: "scopeInfoStr", - versionAdded: 12, - }), - globalCurrencyAuditors: describeStoreV2({ - recordCodec: passthroughCodec<GlobalCurrencyAuditorRecord>(), - storeName: "globalCurrencyAuditors", - keyPath: "id", - autoIncrement: true, - versionAdded: 3, - indexes: { - byCurrencyAndUrlAndPub: describeIndex( - "byCurrencyAndUrlAndPub", - ["currency", "auditorBaseUrl", "auditorPub"], - { - unique: true, - versionAdded: 4, - }, - ), - }, - }), - globalCurrencyExchanges: describeStoreV2({ - recordCodec: passthroughCodec<GlobalCurrencyExchangeRecord>(), - storeName: "globalCurrencyExchanges", - keyPath: "id", - autoIncrement: true, - versionAdded: 3, - indexes: { - byCurrencyAndUrlAndPub: describeIndex( - "byCurrencyAndUrlAndPub", - ["currency", "exchangeBaseUrl", "exchangeMasterPub"], - { - unique: true, - versionAdded: 4, - }, - ), - }, - }), - coinAvailability: describeStore( - "coinAvailability", - describeContents<CoinAvailabilityRecord>({ - keyPath: ["exchangeBaseUrl", "denomPubHash", "maxAge"], - }), - { - byExchangeAgeAvailability: describeIndex("byExchangeAgeAvailability", [ - "exchangeBaseUrl", - "maxAge", - "freshCoinCount", - ]), - byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { - versionAdded: 8, - }), - }, - ), - coinHistory: describeStoreV2({ - storeName: "coinHistory", - recordCodec: passthroughCodec<CoinHistoryRecord>(), - keyPath: "coinPub", - versionAdded: 11, - }), - coins: describeStore( - "coins", - describeContents<CoinRecord>({ - keyPath: "coinPub", - }), - { - byBaseUrl: describeIndex("byBaseUrl", "exchangeBaseUrl"), - byDenomPubHash: describeIndex("byDenomPubHash", "denomPubHash"), - byExchangeDenomPubHashAndAgeAndStatus: describeIndex( - "byExchangeDenomPubHashAndAgeAndStatus", - ["exchangeBaseUrl", "denomPubHash", "maxAge", "status"], - ), - byCoinEvHash: describeIndex("byCoinEvHash", "coinEvHash"), - bySourceTransactionId: describeIndex( - "bySourceTransactionId", - "sourceTransactionId", - { - versionAdded: 9, - }, - ), - }, - ), - tokens: describeStore( - "tokens", - describeContents<TokenRecord>({ - keyPath: "tokenUsePub", - versionAdded: 16, - }), - { - byTokenIssuePubHash: describeIndex( - "byTokenIssuePubHash", - "tokenIssuePubHash", - { - versionAdded: 17, - }, - ), - byPurchaseIdAndChoiceIndex: describeIndex( - "byPurchaseIdAndChoiceIndex", - ["purchaseId", "choiceIndex"], - { - versionAdded: 17, - }, - ), - byTokenFamilyHash: describeIndex("byTokenFamilyHash", "tokenFamilyHash", { - versionAdded: 21, - }), - }, - ), - slates: describeStore( - "slates", - describeContents<SlateRecord>({ - keyPath: "tokenUsePub", - versionAdded: 16, - }), - { - byPurchaseIdAndChoiceIndex: describeIndex( - "byPurchaseIdAndChoiceIndex", - ["purchaseId", "choiceIndex"], - { - versionAdded: 17, - }, - ), - byPurchaseIdAndChoiceIndexAndOutputIndex: describeIndex( - "byPurchaseIdAndChoiceIndexAndOutputIndex", - ["purchaseId", "choiceIndex", "outputIndex"], - { - versionAdded: 17, - }, - ), - byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex: describeIndex( - "byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex", - ["purchaseId", "choiceIndex", "outputIndex", "repeatIndex"], - { - versionAdded: 29, - }, - ), - }, - ), - reserves: describeStore( - "reserves", - describeContents<ReserveRecord>({ - keyPath: "rowId", - autoIncrement: true, - }), - { - byReservePub: describeIndex("byReservePub", "reservePub", {}), - }, - ), - config: describeStore( - "config", - describeContents<ConfigRecord>({ keyPath: "key" }), - {}, - ), - denominations: describeStore( - "denominations", - describeContents<DenominationRecord>({ - keyPath: ["exchangeBaseUrl", "denomPubHash"], - }), - { - byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl"), - byVerificationStatus: describeIndex( - "byVerificationStatus", - "verificationStatus", - { - versionAdded: 26, - }, - ), - byDenominationFamilySerialAndStampExpireWithdraw: describeIndex( - "byDenominationFamilySerialAndStampExpireWithdraw", - ["denominationFamilySerial", "stampExpireWithdraw"], - { - versionAdded: 27, - }, - ), - }, - ), - denominationFamilies: describeStore( - "denominationFamilies", - describeContents<DenominationFamilyRecord>({ - keyPath: "denominationFamilySerial", - versionAdded: 27, - autoIncrement: true, - }), - { - byExchangeBaseUrl: describeIndex( - "byExchangeBaseUrl", - "familyParams.exchangeBaseUrl", - { - versionAdded: 27, - }, - ), - byFamilyParms: describeIndex( - "byFamilyParams", - [ - "familyParams.exchangeBaseUrl", - "familyParams.exchangeMasterPub", - "familyParams.value", - "familyParams.feeWithdraw", - "familyParams.feeDeposit", - "familyParams.feeRefresh", - "familyParams.feeRefund", - ], - { - versionAdded: 28, - }, - ), - // Reserved legacy index names: - // * byFamilyParamsHash - }, - ), - exchanges: describeStore( - "exchanges", - describeContents<ExchangeEntryRecord>({ - keyPath: "baseUrl", - }), - {}, - ), - exchangeDetails: describeStore( - "exchangeDetails", - describeContents<ExchangeDetailsRecord>({ - keyPath: "rowId", - autoIncrement: true, - }), - { - byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { - versionAdded: 2, - }), - byPointer: describeIndex( - "byDetailsPointer", - ["exchangeBaseUrl", "currency", "masterPublicKey"], - { - unique: true, - }, - ), - }, - ), - exchangeSignKeys: describeStore( - "exchangeSignKeys", - describeContents<ExchangeSignkeysRecord>({ - keyPath: ["exchangeDetailsRowId", "signkeyPub"], - }), - { - // Caution: By historical accident, the index is over an array. - byExchangeDetailsRowId: describeIndex("byExchangeDetailsRowId", [ - "exchangeDetailsRowId", - ]), - }, - ), - contacts: describeStoreV2({ - recordCodec: passthroughCodec<ContactRecord>(), - storeName: "contacts", - keyPath: ["alias", "aliasType"], - indexes: {}, - versionAdded: 24, - }), - mailboxMessages: describeStoreV2({ - recordCodec: passthroughCodec<MailboxMessageRecord>(), - storeName: "mailboxMessages", - keyPath: ["originMailboxBaseUrl", "talerUri"], - indexes: {}, - versionAdded: 24, - }), - mailboxConfigurations: describeStoreV2({ - recordCodec: passthroughCodec<MailboxConfiguration>(), - storeName: "mailboxConfigurations", - keyPath: "mailboxBaseUrl", - indexes: {}, - versionAdded: 24, - }), - refreshGroups: describeStore( - "refreshGroups", - describeContents<RefreshGroupRecord>({ - keyPath: "refreshGroupId", - }), - { - byStatus: describeIndex("byStatus", "operationStatus"), - byOriginatingTransactionId: describeIndex( - "byOriginatingTransactionId", - "originatingTransactionId", - { - versionAdded: 5, - }, - ), - }, - ), - refreshSessions: describeStore( - "refreshSessions", - describeContents<RefreshSessionRecord>({ - keyPath: ["refreshGroupId", "coinIndex"], - }), - { - byRefreshGroupId: describeIndex("byRefreshGroupId", "refreshGroupId", { - versionAdded: 15, - }), - }, - ), - recoupGroups: describeStore( - "recoupGroups", - describeContents<RecoupGroupRecord>({ - keyPath: "recoupGroupId", - }), - { - byStatus: describeIndex("byStatus", "operationStatus", { - versionAdded: 6, - }), - byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { - versionAdded: 15, - }), - }, - ), - purchases: describeStore( - "purchases", - describeContents<PurchaseRecord>({ keyPath: "proposalId" }), - { - byStatus: describeIndex("byStatus", "purchaseStatus"), - byFulfillmentUrl: describeIndex( - "byFulfillmentUrl", - "download.fulfillmentUrl", - ), - byUrlAndOrderId: describeIndex("byUrlAndOrderId", [ - "merchantBaseUrl", - "orderId", - ]), - byExchange: describeIndex("byExchange", "exchanges", { - versionAdded: 15, - multiEntry: true, - }), - }, - ), - donationPlanchets: describeStoreV2({ - recordCodec: passthroughCodec<DonationPlanchetRecord>(), - storeName: "donationPlanchets", - keyPath: "udiNonce", - versionAdded: 20, - indexes: { - byProposalId: describeIndex("byProposalId", "proposalId", { - versionAdded: 20, - }), - }, - }), - donationReceipts: describeStoreV2({ - recordCodec: passthroughCodec<DonationReceiptRecord>(), - storeName: "donationReceipts", - keyPath: "udiNonce", - versionAdded: 20, - indexes: { - byStatus: describeIndex("byStatus", "status", { - versionAdded: 20, - }), - byDonauBaseUrl: describeIndex("byDonauBaseUrl", "donauBaseUrl", { - versionAdded: 23, - }), - byStatusAndDonauBaseUrl: describeIndex( - "byStatusAndDonauBaseUrl", - ["status", "donauBaseUrl"], - { - versionAdded: 23, - }, - ), - }, - }), - donationSummaries: describeStoreV2({ - recordCodec: passthroughCodec<DonationSummaryRecord>(), - storeName: "donationSummaries", - keyPath: ["donauBaseUrl", "year", "currency"], - versionAdded: 22, - indexes: {}, - }), - withdrawalGroups: describeStore( - "withdrawalGroups", - describeContents<WithdrawalGroupRecord>({ - keyPath: "withdrawalGroupId", - }), - { - byStatus: describeIndex("byStatus", "status"), - byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { - versionAdded: 2, - }), - byTalerWithdrawUri: describeIndex( - "byTalerWithdrawUri", - "wgInfo.bankInfo.talerWithdrawUri", - ), - }, - ), - planchets: describeStore( - "planchets", - describeContents<PlanchetRecord>({ keyPath: "coinPub" }), - { - byGroupAndIndex: describeIndex( - "byGroupAndIndex", - ["withdrawalGroupId", "coinIdx"], - { - unique: true, - }, - ), - byGroup: describeIndex("byGroup", "withdrawalGroupId"), - byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"), - }, - ), - bankWithdrawUris: describeStore( - "bankWithdrawUris", - describeContents<BankWithdrawUriRecord>({ - keyPath: "talerWithdrawUri", - }), - { - byGroup: describeIndex("byGroup", "withdrawalGroupId"), - }, - ), - depositGroups: describeStore( - "depositGroups", - describeContents<DepositGroupRecord>({ - keyPath: "depositGroupId", - }), - { - byStatus: describeIndex("byStatus", "operationStatus"), - }, - ), - tombstones: describeStore( - "tombstones", - describeContents<TombstoneRecord>({ keyPath: "id" }), - {}, - ), - operationRetries: describeStore( - "operationRetries", - describeContents<OperationRetryRecord>({ - keyPath: "id", - }), - {}, - ), - peerPushCredit: describeStore( - "peerPushCredit", - describeContents<PeerPushCreditRecord>({ - keyPath: "peerPushCreditId", - }), - { - byExchangeAndPurse: describeIndex("byExchangeAndPurse", [ - "exchangeBaseUrl", - "pursePub", - ]), - byExchangeAndContractPriv: describeIndex( - "byExchangeAndContractPriv", - ["exchangeBaseUrl", "contractPriv"], - { - unique: true, - }, - ), - byWithdrawalGroupId: describeIndex( - "byWithdrawalGroupId", - "withdrawalGroupId", - {}, - ), - byStatus: describeIndex("byStatus", "status"), - }, - ), - peerPullDebit: describeStore( - "peerPullDebit", - describeContents<PeerPullPaymentIncomingRecord>({ - keyPath: "peerPullDebitId", - }), - { - byExchangeAndPurse: describeIndex("byExchangeAndPurse", [ - "exchangeBaseUrl", - "pursePub", - ]), - byExchangeAndContractPriv: describeIndex( - "byExchangeAndContractPriv", - ["exchangeBaseUrl", "contractPriv"], - { - unique: true, - }, - ), - byStatus: describeIndex("byStatus", "status"), - }, - ), - peerPullCredit: describeStore( - "peerPullCredit", - describeContents<PeerPullCreditRecord>({ - keyPath: "pursePub", - }), - { - byStatus: describeIndex("byStatus", "status"), - byWithdrawalGroupId: describeIndex( - "byWithdrawalGroupId", - "withdrawalGroupId", - {}, - ), - }, - ), - peerPushDebit: describeStore( - "peerPushDebit", - describeContents<PeerPushDebitRecord>({ - keyPath: "pursePub", - }), - { - byStatus: describeIndex("byStatus", "status"), - }, - ), - bankAccountsV2: describeStore( - "bankAccountsV2", - describeContents<BankAccountsRecord>({ - keyPath: "bankAccountId", - versionAdded: 14, - }), - { - byPaytoUri: describeIndex("byPaytoUri", "paytoUri", { - versionAdded: 14, - }), - }, - ), - contractTerms: describeStore( - "contractTerms", - describeContents<ContractTermsRecord>({ - keyPath: "h", - }), - {}, - ), - refundGroups: describeStore( - "refundGroups", - describeContents<RefundGroupRecord>({ - keyPath: "refundGroupId", - }), - { - byProposalId: describeIndex("byProposalId", "proposalId"), - byStatus: describeIndex("byStatus", "status", {}), - }, - ), - refundItems: describeStore( - "refundItems", - describeContents<RefundItemRecord>({ - keyPath: "id", - autoIncrement: true, - }), - { - byCoinPubAndRtxid: describeIndex("byCoinPubAndRtxid", [ - "coinPub", - "rtxid", - ]), - // FIXME: Why is this a list of index keys? Confusing! - byRefundGroupId: describeIndex("byRefundGroupId", ["refundGroupId"]), - }, - ), - fixups: describeStore( - "fixups", - describeContents<FixupRecord>({ - keyPath: "fixupName", - }), - {}, - ), - // - // Obsolete stores, not used anymore - // - obsolete_backupProviders: describeStore( - "backupProviders", - describeContents<unknown>({ - keyPath: "baseUrl", - }), - { - byPaymentProposalId: describeIndex( - "byPaymentProposalId", - "paymentProposalIds", - { - multiEntry: true, - }, - ), - }, - ), - _obsolete_transactions: describeStoreV2({ - recordCodec: passthroughCodec<unknown>(), - storeName: "transactions", - keyPath: "transactionItem.transactionId", - versionAdded: 7, - indexes: { - byCurrency: describeIndex("byCurrency", "currency", { - versionAdded: 7, - }), - byExchange: describeIndex("byExchange", "exchanges", { - versionAdded: 7, - multiEntry: true, - }), - }, - }), - _obsolete_bankAccounts: describeStore( - "bankAccounts", - describeContents<any>({ - keyPath: "uri", - }), - {}, - ), - _obsolete_rewards: describeStore( - "rewards", - describeContents<any>({ keyPath: "walletRewardId" }), - { - byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [ - "merchantRewardId", - "merchantBaseUrl", - ]), - byStatus: describeIndex("byStatus", "status", { - versionAdded: 8, - }), - }, - ), - obsolete_userAttention: describeStore( - "userAttention", - describeContents<unknown>({ - keyPath: ["entityId", "info.type"], - }), - {}, - ), -}; - -export type WalletIndexedDbTransaction = DbReadWriteTransaction< - typeof WalletIndexedDbStoresV1, - Array<StoreNames<typeof WalletIndexedDbStoresV1>> ->; - -/** - * An applied migration. - */ -export interface FixupRecord { - fixupName: string; -} - -/** - * User accounts - */ -export interface BankAccountsRecord { - /** - * Opaque identifier for the bank account. - */ - bankAccountId: string; - - /** - * Payto URI of the bank account. - */ - paytoUri: string; - - /** - * User-defined label for the account. - */ - label: string | undefined; - - currencies: string[] | undefined; - - /** - * FIXME: Provide more info here. - */ - kycCompleted: boolean; -} - -export interface MetaConfigRecord { - key: string; - value: any; -} - -export const walletMetadataStore = { - metaConfig: describeStore( - "metaConfig", - describeContents<MetaConfigRecord>({ keyPath: "key" }), - {}, - ), -}; - -export interface StoredBackupMeta { - name: string; -} - -export const StoredBackupStores = { - backupMeta: describeStore( - "backupMeta", - describeContents<StoredBackupMeta>({ keyPath: "name" }), - {}, - ), - backupData: describeStore("backupData", describeContents<any>({}), {}), -}; - -export interface DbDumpRecord { - /** - * Key, serialized with structuredEncapsulated. - * - * Only present for out-of-line keys (i.e. no key path). - */ - key?: any; - /** - * Value, serialized with structuredEncapsulated. - */ - value: any; -} - -export interface DbIndexDump { - keyPath: string | string[]; - multiEntry: boolean; - unique: boolean; -} - -export interface DbStoreDump { - keyPath?: string | string[]; - autoIncrement: boolean; - indexes: { [indexName: string]: DbIndexDump }; - records: DbDumpRecord[]; -} - -export interface DbDumpDatabase { - version: number; - stores: { [storeName: string]: DbStoreDump }; -} - -export interface DbDump { - databases: { - [name: string]: DbDumpDatabase; - }; -} - -const logger = new Logger("db.ts"); - -export async function exportSingleDb( - idb: IDBFactory, - dbName: string, -): Promise<DbDumpDatabase> { - const myDb = await openDatabase( - idb, - dbName, - undefined, - () => { - logger.info(`unexpected onversionchange in exportSingleDb of ${dbName}`); - }, - () => { - logger.info(`unexpected onupgradeneeded in exportSingleDb of ${dbName}`); - }, - ); - - const singleDbDump: DbDumpDatabase = { - version: myDb.version, - stores: {}, - }; - - return new Promise((resolve, reject) => { - const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); - tx.addEventListener("complete", () => { - //myDb.close(); - resolve(singleDbDump); - }); - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < myDb.objectStoreNames.length; i++) { - const name = myDb.objectStoreNames[i]; - const store = tx.objectStore(name); - const storeDump: DbStoreDump = { - autoIncrement: store.autoIncrement, - keyPath: store.keyPath, - indexes: {}, - records: [], - }; - const indexNames = store.indexNames; - for (let j = 0; j < indexNames.length; j++) { - const idxName = indexNames[j]; - const index = store.index(idxName); - storeDump.indexes[idxName] = { - keyPath: index.keyPath, - multiEntry: index.multiEntry, - unique: index.unique, - }; - } - singleDbDump.stores[name] = storeDump; - store.openCursor().addEventListener("success", (e: Event) => { - const cursor = (e.target as any).result; - if (cursor) { - const rec: DbDumpRecord = { - value: structuredEncapsulate(cursor.value), - }; - // Only store key if necessary, i.e. when - // the key is not stored as part of the object via - // a key path. - if (store.keyPath == null) { - rec.key = structuredEncapsulate(cursor.key); - } - storeDump.records.push(rec); - cursor.continue(); - } - }); - } - }); -} - -export async function exportDb(idb: IDBFactory): Promise<DbDump> { - const dbDump: DbDump = { - databases: {}, - }; - - dbDump.databases[TALER_WALLET_META_DB_NAME] = await exportSingleDb( - idb, - TALER_WALLET_META_DB_NAME, - ); - dbDump.databases[TALER_WALLET_MAIN_DB_NAME] = await exportSingleDb( - idb, - TALER_WALLET_MAIN_DB_NAME, - ); - - return dbDump; -} - -async function recoverFromDump( - db: IDBDatabase, - dbDump: DbDumpDatabase, -): Promise<void> { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - const txProm = promiseFromTransaction(tx); - const storeNames = db.objectStoreNames; - for (let i = 0; i < storeNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = dbDump.stores[name]; - if (!storeDump) continue; - await promiseFromRequest(tx.objectStore(name).clear()); - logger.info(`importing ${storeDump.records.length} records into ${name}`); - for (let rec of storeDump.records) { - await promiseFromRequest(tx.objectStore(name).put(rec.value, rec.key)); - logger.trace("importing record done"); - } - } - tx.commit(); - return await txProm; -} - -function checkDbDump(x: any): x is DbDump { - return "databases" in x; -} - -export async function importDb(db: IDBDatabase, dumpJson: any): Promise<void> { - const d = structuredRevive(dumpJson); - if (checkDbDump(d)) { - const walletDb = d.databases[TALER_WALLET_MAIN_DB_NAME]; - if (!walletDb) { - throw Error( - `unable to import, main wallet database (${TALER_WALLET_MAIN_DB_NAME}) not found`, - ); - } - await recoverFromDump(db, walletDb); - } else { - throw Error("unable to import, doesn't look like a valid DB dump"); - } -} - -export interface FixupDescription { - name: string; - fn(tx: WalletIndexedDbTransaction): Promise<void>; -} - -/** - * Manual migrations between minor versions of the DB schema. - * - * Fixups *must* be idempotent. - */ -export const walletDbFixups: FixupDescription[] = [ - // Removing this would cause old transactions - // to show up under multiple exchanges - { - fn: fixup20250915TransactionsScope, - name: "fixup20250915TransactionsScope", - }, - // Removing this would cause merchant acceptable - // amount to be calculaed based on exchangeBaseUrl - // instead of masterPublicKey for old coins. - { - fn: fixupCoinAvailabilityExchangePub, - name: "fixupCoinAvailabilityExchangePub", - }, - // Can be removed once all affected refresh groups have - // been fixed. Conservative estimate: Jan 2028. - { - fn: fixup20260116BadRefreshCoinSelection, - name: "fixup20260116BadRefreshCoinSelection", - }, - // Denom families were introduced. - // This migration creates denom families - // for existing denomination records. - { - fn: fixup20260203DenomFamilyMigration, - name: "fixup20260203DenomFamilyMigration", - }, - // Fix a problem where refreshes went into a failed state - // instead of retrying. - { - fn: fixup20260213RefreshBlunder, - name: "fixup20260213RefreshBlunder", - }, -]; - -async function fixup20260213RefreshBlunder( - tx: WalletIndexedDbTransaction, -): Promise<void> { - const refreshes = await tx.refreshGroups.indexes.byStatus.getAll( - RefreshOperationStatus.Failed, - ); - for (const refreshGroup of refreshes) { - for ( - let coinIndex = 0; - coinIndex < refreshGroup.statusPerCoin.length; - coinIndex++ - ) { - let changed = false; - if (refreshGroup.statusPerCoin[coinIndex] === RefreshCoinStatus.Failed) { - const rs = await tx.refreshSessions.get([ - refreshGroup.refreshGroupId, - coinIndex, - ]); - if ( - rs?.lastError?.code === - TalerErrorCode.EXCHANGE_GENERIC_DENOMINATION_EXPIRED - ) { - refreshGroup.statusPerCoin[coinIndex] = - RefreshCoinStatus.PendingRedenominate; - refreshGroup.operationStatus = - RefreshOperationStatus.PendingRedenominate; - delete refreshGroup.timestampFinished; - changed = true; - } - } - if (changed) { - await tx.refreshGroups.put(refreshGroup); - } - } - } -} - -async function fixup20260203DenomFamilyMigration( - tx: WalletIndexedDbTransaction, -): Promise<void> { - const batchSize = 500; - - let range: IDBKeyRange | undefined = undefined; - - while (1) { - const batch = await tx.denominations.getAll(range, batchSize); - - if (batch.length === 0) { - break; - } - - logger.info(`fixing up batch of ${batch.length} denominations`); - - const last = batch[batch.length - 1]; - range = GlobalIDB.KeyRange.lowerBound( - [last.exchangeBaseUrl, last.denomPubHash], - true, - ); - - for (const r of batch) { - const fp: DenomFamilyParams = { - exchangeBaseUrl: r.exchangeBaseUrl, - exchangeMasterPub: r.exchangeMasterPub, - feeDeposit: r.fees.feeDeposit, - feeRefresh: r.fees.feeRefresh, - feeRefund: r.fees.feeRefund, - feeWithdraw: r.fees.feeWithdraw, - value: r.value, - }; - if (r.denominationFamilySerial != null) { - // Fast path: Check if family exists and is correct. - const oldFpRec = await tx.denominationFamilies.get( - r.denominationFamilySerial, - ); - if ( - oldFpRec && - canonicalJson(fp) == canonicalJson(oldFpRec.familyParams) - ) { - continue; - } - } - const familyParamsIndexKey = [ - fp.exchangeBaseUrl, - fp.exchangeMasterPub, - fp.value, - fp.feeWithdraw, - fp.feeDeposit, - fp.feeRefresh, - fp.feeRefund, - ]; - const dfRec = - await tx.denominationFamilies.indexes.byFamilyParms.get( - familyParamsIndexKey, - ); - let denominationFamilySerial; - if (dfRec) { - denominationFamilySerial = dfRec.denominationFamilySerial; - } else { - const insRes = await tx.denominationFamilies.put({ - familyParams: fp, - }); - denominationFamilySerial = insRes.key; - } - checkDbInvariant( - typeof denominationFamilySerial == "number", - "denominationFamilySerial", - ); - r.denominationFamilySerial = denominationFamilySerial; - await tx.denominations.put(r); - } - } -} - -async function fixup20260116BadRefreshCoinSelection( - tx: WalletIndexedDbTransaction, -): Promise<void> { - await tx.refreshGroups.iter().forEachAsync(async (rec) => { - const inputAmount = Amounts.sumOrZero( - rec.currency, - rec.inputPerCoin, - ).amount; - const outputAmount = Amounts.sumOrZero( - rec.currency, - rec.expectedOutputPerCoin, - ).amount; - if (Amounts.isNonZero(inputAmount) && Amounts.isZero(outputAmount)) { - logger.info( - `fixing up refresh group ${rec.refreshGroupId}, setting status to PendingRedenominate`, - ); - rec.operationStatus = RefreshOperationStatus.PendingRedenominate; - delete rec.timestampFinished; - await tx.refreshGroups.put(rec); - } - }); -} - -/** - * Some old payment transactions didn't correctly - * set the involved exchanges. - * - * This fixup sets the exchanges of a payment transaction - * based on the coin selection. - */ -async function fixup20250915TransactionsScope( - tx: WalletIndexedDbTransaction, -): Promise<void> { - await tx.purchases.iter().forEachAsync(async (rec) => { - if ( - (rec.exchanges?.length ?? 0) == 0 && - rec.payInfo?.payCoinSelection != null - ) { - const pcs = rec.payInfo.payCoinSelection.coinPubs; - const exchSet: Set<string> = new Set(); - for (const pc in pcs) { - const coin = await tx.coins.get(pc); - if (!coin) { - continue; - } - exchSet.add(coin.exchangeBaseUrl); - } - rec.exchanges = [...exchSet]; - rec.exchanges.sort(); - if (rec.exchanges.length == 0) { - // For old SPURLOS transactions, set exchange manually - // when we can't infer it. - if ( - rec.timestamp <= 1736942400000_000 && - rec.download?.currency === "SPURLOS" - ) { - rec.exchanges = ["https://exchange.taler.datenspuren.de/"]; - } - logger.warn( - `unable to fix up pay transaction ${rec.proposalId}, could not reconstruct exchanges`, - ); - } - await tx.purchases.put(rec); - } - }); -} - -async function fixupCoinAvailabilityExchangePub( - tx: WalletIndexedDbTransaction, -): Promise<void> { - const cars = await tx.coinAvailability.getAll(); - const exchanges: Record<string, ExchangeDetailsRecord | undefined> = {}; - for (const car of cars) { - if (car.exchangeMasterPub === undefined) { - if (exchanges[car.exchangeBaseUrl] === undefined) { - exchanges[car.exchangeBaseUrl] = - await tx.exchangeDetails.indexes.byExchangeBaseUrl.get( - car.exchangeBaseUrl, - ); - } - - const exchange = exchanges[car.exchangeBaseUrl]; - if (exchange !== undefined) { - car.exchangeMasterPub = exchange.masterPublicKey; - await tx.coinAvailability.put(car); - } - } - } -} - -export async function applyFixups( - db: DbAccess<typeof WalletIndexedDbStoresV1>, -): Promise<number> { - logger.trace("applying fixups"); - let count = 0; - for (const fixupInstruction of walletDbFixups) { - await db.runAllStoresReadWriteTx({}, async (tx) => { - logger.trace(`checking fixup ${fixupInstruction.name}`); - const fixupRecord = await tx.fixups.get(fixupInstruction.name); - if (fixupRecord) { - return; - } - logger.trace(`applying DB fixup ${fixupInstruction.name}`); - await fixupInstruction.fn(tx); - await tx.fixups.put({ - fixupName: fixupInstruction.name, - }); - count++; - }); - } - return count; -} - -/** - * Upgrade an IndexedDB in an upgrade transaction. - * - * The upgrade is made based on a store map, i.e. the metadata - * structure that describes all the object stores and indexes. - */ -function upgradeFromStoreMap( - storeMap: any, // FIXME: nail down type - db: IDBDatabase, - oldVersion: number, - newVersion: number, - upgradeTransaction: IDBTransaction, -): void { - if (oldVersion === 0) { - for (const n in storeMap) { - const swi: StoreWithIndexes< - any, - StoreDescriptor<unknown>, - any - > = storeMap[n]; - const storeDesc: StoreDescriptor<unknown> = swi.store; - const s = db.createObjectStore(swi.storeName, { - autoIncrement: storeDesc.autoIncrement, - keyPath: storeDesc.keyPath, - }); - for (const indexName in swi.indexMap as any) { - const indexDesc: IndexDescriptor = swi.indexMap[indexName]; - s.createIndex(indexDesc.name, indexDesc.keyPath, { - multiEntry: indexDesc.multiEntry, - unique: indexDesc.unique, - }); - } - } - return; - } - if (oldVersion === newVersion) { - return; - } - logger.info(`upgrading database from ${oldVersion} to ${newVersion}`); - for (const n in storeMap) { - const swi: StoreWithIndexes<any, StoreDescriptor<unknown>, any> = storeMap[ - n - ]; - const storeDesc: StoreDescriptor<unknown> = swi.store; - const storeAddedVersion = storeDesc.versionAdded ?? 0; - let s: IDBObjectStore; - if (storeAddedVersion > oldVersion) { - // Be tolerant if object store already exists. - // Probably means somebody deployed without - // adding the "addedInVersion" attribute. - if (!upgradeTransaction.objectStoreNames.contains(swi.storeName)) { - try { - s = db.createObjectStore(swi.storeName, { - autoIncrement: storeDesc.autoIncrement, - keyPath: storeDesc.keyPath, - }); - } catch (e) { - const moreInfo = e instanceof Error ? ` Reason: ${e.message}` : ""; - throw new Error( - `Migration failed. Could not create store ${swi.storeName}.${moreInfo}`, - // @ts-expect-error no support for options.cause yet - { cause: e }, - ); - } - } - } - - s = upgradeTransaction.objectStore(swi.storeName); - - for (const indexName in swi.indexMap as any) { - const indexDesc: IndexDescriptor = swi.indexMap[indexName]; - const indexAddedVersion = indexDesc.versionAdded ?? 0; - if (indexAddedVersion <= oldVersion) { - continue; - } - // Be tolerant if index already exists. - // Probably means somebody deployed without - // adding the "addedInVersion" attribute. - if (!s.indexNames.contains(indexDesc.name)) { - try { - s.createIndex(indexDesc.name, indexDesc.keyPath, { - multiEntry: indexDesc.multiEntry, - unique: indexDesc.unique, - }); - } catch (e) { - const moreInfo = e instanceof Error ? ` Reason: ${e.message}` : ""; - throw Error( - `Migration failed. Could not create index ${indexDesc.name}/${indexDesc.keyPath}. ${moreInfo}`, - // @ts-expect-error no support for options.cause yet - { cause: e }, - ); - } - } - } - } -} - -function promiseFromTransaction(transaction: IDBTransaction): Promise<void> { - return new Promise<void>((resolve, reject) => { - transaction.oncomplete = () => { - resolve(); - }; - transaction.onerror = () => { - reject(); - }; - }); -} - -export function promiseFromRequest(request: IDBRequest): Promise<any> { - return new Promise((resolve, reject) => { - request.onsuccess = () => { - resolve(request.result); - }; - request.onerror = () => { - reject(request.error); - }; - }); -} - -/** - * Purge all data in the given database. - */ -export function clearDatabase(db: IDBDatabase): Promise<void> { - // db.objectStoreNames is a DOMStringList, so we need to convert - let stores: string[] = []; - for (let i = 0; i < db.objectStoreNames.length; i++) { - stores.push(db.objectStoreNames[i]); - } - logger.info(`clearing object stores: ${j2s(stores)}`); - const tx = db.transaction(stores, "readwrite"); - for (const store of stores) { - tx.objectStore(store).clear(); - } - return promiseFromTransaction(tx); -} - -function onTalerDbUpgradeNeeded( - db: IDBDatabase, - oldVersion: number, - newVersion: number, - upgradeTransaction: IDBTransaction, -) { - upgradeFromStoreMap( - WalletIndexedDbStoresV1, - db, - oldVersion, - newVersion, - upgradeTransaction, - ); -} - -function onMetaDbUpgradeNeeded( - db: IDBDatabase, - oldVersion: number, - newVersion: number, - upgradeTransaction: IDBTransaction, -) { - upgradeFromStoreMap( - walletMetadataStore, - db, - oldVersion, - newVersion, - upgradeTransaction, - ); -} - -function onStoredBackupsDbUpgradeNeeded( - db: IDBDatabase, - oldVersion: number, - newVersion: number, - upgradeTransaction: IDBTransaction, -) { - upgradeFromStoreMap( - StoredBackupStores, - db, - oldVersion, - newVersion, - upgradeTransaction, - ); -} - -export async function openStoredBackupsDatabase( - idbFactory: IDBFactory, -): Promise<DbAccess<typeof StoredBackupStores>> { - const backupsDbHandle = await openDatabase( - idbFactory, - TALER_WALLET_STORED_BACKUPS_DB_NAME, - 1, - () => {}, - onStoredBackupsDbUpgradeNeeded, - ); - - const handle = new DbAccessImpl( - backupsDbHandle, - StoredBackupStores, - {}, - CancellationToken.CONTINUE, - ); - return handle; -} - -/** - * Return a promise that resolves - * to the taler wallet db. - * - * @param onVersionChange Called when another client concurrenctly connects to the database - * with a higher version. - */ -export async function openTalerDatabase( - idbFactory: IDBFactory, - onVersionChange: () => void, -): Promise<IDBDatabase> { - const metaDbHandle = await openDatabase( - idbFactory, - TALER_WALLET_META_DB_NAME, - 1, - () => {}, - onMetaDbUpgradeNeeded, - ); - - const metaDb = new DbAccessImpl( - metaDbHandle, - walletMetadataStore, - {}, - CancellationToken.CONTINUE, - ); - let currentMainVersion: string | undefined; - await metaDb.runAllStoresReadWriteTx({}, async (tx) => { - const dbVersionRecord = await tx.metaConfig.get(CURRENT_DB_CONFIG_KEY); - if (!dbVersionRecord) { - currentMainVersion = TALER_WALLET_MAIN_DB_NAME; - await tx.metaConfig.put({ - key: CURRENT_DB_CONFIG_KEY, - value: TALER_WALLET_MAIN_DB_NAME, - }); - } else { - currentMainVersion = dbVersionRecord.value; - } - }); - - if (currentMainVersion !== TALER_WALLET_MAIN_DB_NAME) { - switch (currentMainVersion) { - case "taler-wallet-main-v2": - case "taler-wallet-main-v3": - case "taler-wallet-main-v4": // temporary, we might migrate v4 later - case "taler-wallet-main-v5": - case "taler-wallet-main-v6": - case "taler-wallet-main-v7": - case "taler-wallet-main-v8": - case "taler-wallet-main-v9": - // We consider this a pre-release - // development version, no migration is done. - await metaDb.runAllStoresReadWriteTx({}, async (tx) => { - await tx.metaConfig.put({ - key: CURRENT_DB_CONFIG_KEY, - value: TALER_WALLET_MAIN_DB_NAME, - }); - }); - break; - default: - throw Error( - `major migration from database major=${currentMainVersion} not supported`, - ); - } - } - - const mainDbHandle = await openDatabase( - idbFactory, - TALER_WALLET_MAIN_DB_NAME, - WALLET_DB_MINOR_VERSION, - onVersionChange, - onTalerDbUpgradeNeeded, - ); - - return mainDbHandle; -} - -export async function deleteTalerDatabase( - idbFactory: IDBFactory, -): Promise<void> { - return new Promise((resolve, reject) => { - const req = idbFactory.deleteDatabase(TALER_WALLET_MAIN_DB_NAME); - req.onerror = () => reject(req.error); - req.onsuccess = () => resolve(); - }); -} diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -19,7 +19,7 @@ import { ScopeInfo, stringifyScopeInfo, } from "@gnu-taler/taler-util"; -import { ConfigRecord, WalletIndexedDbTransaction } from "./db.js"; +import { ConfigRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; export interface GetCurrencyInfoDbResult { /** diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts @@ -33,10 +33,10 @@ import { Logger, } from "@gnu-taler/taler-util"; import { - DenominationRecord, DenominationVerificationStatus, timestampAbsoluteFromDb, -} from "./db.js"; +} from "./db-common.js"; +import { DenominationRecord } from "./db-indexeddb.js"; import { isWithdrawableDenom } from "./denominations.js"; const logger = new Logger("denomSelection.ts"); diff --git a/packages/taler-wallet-core/src/denominations.ts b/packages/taler-wallet-core/src/denominations.ts @@ -33,10 +33,10 @@ import { } from "@gnu-taler/taler-util"; import { TaskRunResult } from "./common.js"; import { - DenominationRecord, DenominationVerificationStatus, timestampProtocolFromDb, -} from "./db.js"; +} from "./db-common.js"; +import { DenominationRecord } from "./db-indexeddb.js"; import { WalletExecutionContext } from "./index.js"; /** diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -109,21 +109,22 @@ import { } from "./common.js"; import { DepositElementStatus, - DepositGroupRecord, - DepositInfoPerExchange, DepositOperationStatus, - DepositTrackingInfo, RefreshOperationStatus, - WalletIndexedDbTransaction, - WithdrawalGroupRecord, - WithdrawalGroupStatus, - WithdrawalRecordType, timestampAbsoluteFromDb, timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, timestampProtocolToDb, -} from "./db.js"; +} from "./db-common.js"; +import { + DepositGroupRecord, + DepositInfoPerExchange, + DepositTrackingInfo, + WalletIndexedDbTransaction, + WithdrawalGroupRecord, + WithdrawalRecordType, +} from "./db-indexeddb.js"; import { ReadyExchangeSummary, fetchFreshExchange, @@ -131,7 +132,11 @@ import { getExchangeWireFee, getScopeForAllExchanges, } from "./exchanges.js"; -import { EddsaKeyPairStrings, SignContractTermsHashResponse } from "./index.js"; +import { + EddsaKeyPairStrings, + SignContractTermsHashResponse, + WithdrawalGroupStatus, +} from "./index.js"; import { GenericKycStatusReq, checkDepositHardLimitExceeded, diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -55,18 +55,20 @@ import { } from "@gnu-taler/taler-util/http"; import { PendingTaskType, constructTaskIdentifier } from "./common.js"; import { - DenomLossEventRecord, DenomLossStatus, PeerPushCreditStatus, PeerPushDebitStatus, PurchaseStatus, - RefreshGroupRecord, RefreshOperationStatus, WithdrawalGroupStatus, - WithdrawalRecordType, timestampPreciseToDb, timestampProtocolToDb, -} from "./db.js"; +} from "./db-common.js"; +import { + DenomLossEventRecord, + RefreshGroupRecord, + WithdrawalRecordType, +} from "./db-indexeddb.js"; import { DenomLossTransactionContext, fetchFreshExchange, diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts @@ -53,12 +53,11 @@ import { stringToBytes, succeedOrThrow, } from "@gnu-taler/taler-util"; +import { ConfigRecordKey, DonationReceiptStatus } from "./db-common.js"; import { - ConfigRecordKey, DonationPlanchetRecord, DonationReceiptRecord, - DonationReceiptStatus, -} from "./db.js"; +} from "./db-indexeddb.js"; import { WalletExecutionContext } from "./index.js"; /** diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -137,27 +137,29 @@ import { getRetryDuration, } from "./common.js"; import { - DenomFamilyParams, - DenomLossEventRecord, DenomLossStatus, - DenominationFamilyRecord, - DenominationRecord, DenominationVerificationStatus, - ExchangeDetailsRecord, ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, - ExchangeEntryRecord, - ExchangeMigrationReason, - ReserveRecord, ReserveRecordStatus, - WalletIndexedDbTransaction, timestampAbsoluteFromDb, timestampOptionalPreciseFromDb, timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, timestampProtocolToDb, -} from "./db.js"; +} from "./db-common.js"; +import { + DenomFamilyParams, + DenomLossEventRecord, + DenominationFamilyRecord, + DenominationRecord, + ExchangeDetailsRecord, + ExchangeEntryRecord, + ExchangeMigrationReason, + ReserveRecord, + WalletIndexedDbTransaction, +} from "./db-indexeddb.js"; import { createTimeline, isCandidateWithdrawableDenomRec, diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts @@ -38,13 +38,14 @@ export { parseTransactionIdentifier } from "./transactions.js"; export { createPairTimeline } from "./denominations.js"; // FIXME: Should these really be exported?! +export { WithdrawalGroupStatus } from "./db-common.js"; export { deleteTalerDatabase, exportDb, importDb, WalletIndexedDbStoresV1 as WalletStoresV1, - WithdrawalGroupStatus, -} from "./db.js"; +} from "./db-indexeddb.js"; + export { DbAccess } from "./query.js"; export { TaskRunResult, TaskRunResultType } from "./common.js"; diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts @@ -28,7 +28,8 @@ import { checkDbInvariant, strcmp, } from "@gnu-taler/taler-util"; -import { DenominationRecord, timestampProtocolFromDb } from "./db.js"; +import { timestampProtocolFromDb } from "./db-common.js"; +import { DenominationRecord } from "./db-indexeddb.js"; import { getExchangeDetailsInTx } from "./exchanges.js"; import { WalletExecutionContext } from "./wallet.js"; diff --git a/packages/taler-wallet-core/src/kyc.ts b/packages/taler-wallet-core/src/kyc.ts @@ -40,7 +40,7 @@ import { DbPreciseTimestamp, timestampAbsoluteFromDb, timestampPreciseToDb, -} from "./db.js"; +} from "./db-common.js"; import { ReadyExchangeSummary } from "./exchanges.js"; import { WalletExecutionContext } from "./index.js"; diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -142,26 +142,28 @@ import { } from "./common.js"; import { EddsaKeyPairStrings } from "./crypto/cryptoImplementation.js"; import { - CoinRecord, ConfigRecordKey, + PurchaseStatus, + RefundGroupStatus, + RefundItemStatus, + timestampPreciseFromDb, + timestampPreciseToDb, + timestampProtocolFromDb, + timestampProtocolToDb, +} from "./db-common.js"; +import { + CoinRecord, DbCoinSelection, DenominationRecord, DonationPlanchetRecord, PurchaseRecord, - PurchaseStatus, RefundGroupRecord, - RefundGroupStatus, RefundItemRecord, - RefundItemStatus, RefundReason, SlateRecord, - timestampPreciseFromDb, - timestampPreciseToDb, - timestampProtocolFromDb, - timestampProtocolToDb, TokenRecord, WalletIndexedDbTransaction, -} from "./db.js"; +} from "./db-indexeddb.js"; import { acceptDonauBlindSigs, generateDonauPlanchets } from "./donau.js"; import { getExchangeScopeInfoOrUndefined, diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -28,7 +28,7 @@ import { DbPeerPushPaymentCoinSelection, ReserveRecord, WalletIndexedDbTransaction, -} from "./db.js"; +} from "./db-indexeddb.js"; import { getTotalRefreshCost } from "./refresh.js"; import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; import { updateWithdrawalDenomsForExchange } from "./withdraw.js"; diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -65,16 +65,18 @@ import { runWithClientCancellation, } from "./common.js"; import { + PeerPullPaymentCreditStatus, + WithdrawalGroupStatus, + timestampPreciseFromDb, + timestampPreciseToDb, +} from "./db-common.js"; +import { OperationRetryRecord, PeerPullCreditRecord, - PeerPullPaymentCreditStatus, WalletIndexedDbTransaction, WithdrawalGroupRecord, - WithdrawalGroupStatus, WithdrawalRecordType, - timestampPreciseFromDb, - timestampPreciseToDb, -} from "./db.js"; +} from "./db-indexeddb.js"; import { BalanceThresholdCheckResult, checkIncomingAmountLegalUnderKycBalanceThreshold, diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -76,12 +76,14 @@ import { } from "./common.js"; import { PeerPullDebitRecordStatus, - PeerPullPaymentIncomingRecord, RefreshOperationStatus, - WalletIndexedDbTransaction, timestampPreciseFromDb, timestampPreciseToDb, -} from "./db.js"; +} from "./db-common.js"; +import { + PeerPullPaymentIncomingRecord, + WalletIndexedDbTransaction, +} from "./db-indexeddb.js"; import { getExchangeScopeInfo, getScopeForAllExchanges } from "./exchanges.js"; import { getTotalPeerPaymentCost, diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -67,16 +67,18 @@ import { requireExchangeTosAcceptedOrThrow, } from "./common.js"; import { + PeerPushCreditStatus, + WithdrawalGroupStatus, + timestampPreciseFromDb, + timestampPreciseToDb, +} from "./db-common.js"; +import { OperationRetryRecord, PeerPushCreditRecord, - PeerPushCreditStatus, WalletIndexedDbTransaction, WithdrawalGroupRecord, - WithdrawalGroupStatus, WithdrawalRecordType, - timestampPreciseFromDb, - timestampPreciseToDb, -} from "./db.js"; +} from "./db-indexeddb.js"; import { BalanceThresholdCheckResult, checkIncomingAmountLegalUnderKycBalanceThreshold, diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -75,14 +75,16 @@ import { } from "./common.js"; import { EncryptContractRequest } from "./crypto/cryptoTypes.js"; import { - PeerPushDebitRecord, PeerPushDebitStatus, - WalletIndexedDbTransaction, timestampPreciseFromDb, timestampPreciseToDb, timestampProtocolFromDb, timestampProtocolToDb, -} from "./db.js"; +} from "./db-common.js"; +import { + PeerPushDebitRecord, + WalletIndexedDbTransaction, +} from "./db-indexeddb.js"; import { fetchFreshExchange, getExchangeDetailsInTx, diff --git a/packages/taler-wallet-core/src/preset-exchanges.ts b/packages/taler-wallet-core/src/preset-exchanges.ts @@ -15,7 +15,7 @@ */ import { CurrencySpecification, Logger } from "@gnu-taler/taler-util"; -import { ConfigRecordKey } from "./db.js"; +import { ConfigRecordKey } from "./db-common.js"; import { putPresetExchangeEntry } from "./exchanges.js"; import { WalletExecutionContext } from "./wallet.js"; diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts @@ -52,17 +52,19 @@ import { constructTaskIdentifier, } from "./common.js"; import { + RecoupOperationStatus, + WithdrawalGroupStatus, + timestampPreciseToDb, +} from "./db-common.js"; +import { CoinRecord, CoinSourceType, RecoupGroupRecord, - RecoupOperationStatus, RefreshCoinSource, WalletIndexedDbTransaction, WithdrawCoinSource, - WithdrawalGroupStatus, WithdrawalRecordType, - timestampPreciseToDb, -} from "./db.js"; +} from "./db-indexeddb.js"; import { createRefreshGroup } from "./refresh.js"; import { constructTransactionIdentifier } from "./transactions.js"; import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -91,20 +91,22 @@ import { import { RefreshNewDenomInfo } from "./crypto/cryptoTypes.js"; import { CryptoApiStoppedError } from "./crypto/workers/crypto-dispatcher.js"; import { + RefreshCoinStatus, + RefreshOperationStatus, + timestampPreciseFromDb, + timestampPreciseToDb, +} from "./db-common.js"; +import { CoinAvailabilityRecord, CoinHistoryRecord, CoinRecord, CoinSourceType, DenominationRecord, - RefreshCoinStatus, RefreshGroupPerExchangeInfo, RefreshGroupRecord, - RefreshOperationStatus, RefreshSessionRecord, - timestampPreciseFromDb, - timestampPreciseToDb, WalletIndexedDbTransaction, -} from "./db.js"; +} from "./db-indexeddb.js"; import { selectWithdrawalDenominations } from "./denomSelection.js"; import { fetchFreshExchange, diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts @@ -53,13 +53,15 @@ import { import { OPERATION_STATUS_NONFINAL_FIRST, OPERATION_STATUS_NONFINAL_LAST, - OperationRetryRecord, PurchaseStatus, ReserveRecordStatus, - WalletIndexedDbTransaction, timestampAbsoluteFromDb, timestampPreciseToDb, -} from "./db.js"; +} from "./db-common.js"; +import { + OperationRetryRecord, + WalletIndexedDbTransaction, +} from "./db-indexeddb.js"; import { processValidateDenoms } from "./denominations.js"; import { computeDepositTransactionStatus, diff --git a/packages/taler-wallet-core/src/tokenFamilies.ts b/packages/taler-wallet-core/src/tokenFamilies.ts @@ -29,7 +29,7 @@ import { MerchantInfo, SubscriptionListDetail, } from "@gnu-taler/taler-util"; -import { TokenRecord } from "./db.js"; +import { TokenRecord } from "./db-indexeddb.js"; import { WalletExecutionContext } from "./index.js"; import { expectProposalDownloadInTx } from "./pay-merchant.js"; import { isTokenInUse, isTokenValid } from "./tokenSelection.js"; diff --git a/packages/taler-wallet-core/src/tokenSelection.ts b/packages/taler-wallet-core/src/tokenSelection.ts @@ -28,11 +28,8 @@ import { TalerProtocolTimestamp, TokenAvailabilityHint, } from "@gnu-taler/taler-util"; -import { - timestampProtocolFromDb, - TokenRecord, - WalletIndexedDbTransaction, -} from "./db.js"; +import { timestampProtocolFromDb } from "./db-common.js"; +import { TokenRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; import { WalletExecutionContext } from "./index.js"; const logger = new Logger("tokenSelection.ts"); diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -57,9 +57,11 @@ import { OPERATION_STATUS_NONFINAL_LAST, PurchaseStatus, timestampPreciseToDb, +} from "./db-common.js"; +import { TransactionMetaRecord, WalletIndexedDbTransaction, -} from "./db.js"; +} from "./db-indexeddb.js"; import { DepositTransactionContext } from "./deposits.js"; import { DenomLossTransactionContext } from "./exchanges.js"; import { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -309,8 +309,12 @@ import { CryptoWorkerFactory, } from "./crypto/workers/crypto-dispatcher.js"; import { - CoinSourceType, ConfigRecordKey, + timestampAbsoluteFromDb, + timestampProtocolToDb, +} from "./db-common.js"; +import { + CoinSourceType, DenominationRecord, WalletIndexedDbStoresV1, WalletIndexedDbTransaction, @@ -320,10 +324,8 @@ import { importDb, openStoredBackupsDatabase, openTalerDatabase, - timestampAbsoluteFromDb, - timestampProtocolToDb, walletDbFixups, -} from "./db.js"; +} from "./db-indexeddb.js"; import { IdbWalletTransaction, WalletDbTransaction } from "./dbtx.js"; import { isCandidateWithdrawableDenomRec, diff --git a/packages/taler-wallet-core/src/withdraw.test.ts b/packages/taler-wallet-core/src/withdraw.test.ts @@ -21,13 +21,13 @@ import { DenomKeyType, Duration, } from "@gnu-taler/taler-util"; -import { test } from "node:test"; import assert from "node:assert"; +import { test } from "node:test"; import { - DenominationRecord, DenominationVerificationStatus, timestampProtocolToDb, -} from "./db.js"; +} from "./db-common.js"; +import { DenominationRecord } from "./db-indexeddb.js"; import { selectWithdrawalDenominations } from "./denomSelection.js"; test("withdrawal selection bug repro", (t) => { diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -139,20 +139,13 @@ import { CoinRecord, CoinSourceType, DenominationRecord, - DenominationVerificationStatus, OperationRetryRecord, PlanchetRecord, - PlanchetStatus, WalletIndexedDbTransaction, WgInfo, WithdrawalGroupRecord, - WithdrawalGroupStatus, WithdrawalRecordType, - timestampAbsoluteFromDb, - timestampPreciseFromDb, - timestampPreciseToDb, - timestampProtocolToDb, -} from "./db.js"; +} from "./db-indexeddb.js"; import { selectForcedWithdrawalDenominations, selectWithdrawalDenominations, @@ -192,6 +185,15 @@ import { } from "./transactions.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js"; import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; +import { + WithdrawalGroupStatus, + timestampPreciseFromDb, + timestampProtocolToDb, + PlanchetStatus, + timestampPreciseToDb, + DenominationVerificationStatus, + timestampAbsoluteFromDb, +} from "./db-common.js"; /** * Logger for this file.