summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/db.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/db.ts')
-rw-r--r--packages/taler-wallet-core/src/db.ts3114
1 files changed, 2373 insertions, 741 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 902f749cf..1edafb315 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2021-2024 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
@@ -18,27 +18,101 @@
* Imports.
*/
import {
- describeStore,
- describeContents,
- describeIndex,
-} from "./util/query.js";
+ Event,
+ IDBDatabase,
+ IDBFactory,
+ IDBObjectStore,
+ IDBRequest,
+ IDBTransaction,
+ structuredEncapsulate,
+ structuredRevive,
+} from "@gnu-taler/idb-bridge";
import {
- AmountJson,
+ AbsoluteTime,
+ AgeCommitmentProof,
AmountString,
- Auditor,
- CoinDepositPermission,
- ContractTerms,
- Duration,
- ExchangeSignKeyJson,
- InternationalizedString,
- MerchantInfo,
- Product,
+ Amounts,
+ AttentionInfo,
+ BackupProviderTerms,
+ CancellationToken,
+ Codec,
+ CoinEnvelope,
+ CoinPublicKeyString,
+ CoinRefreshRequest,
+ CoinStatus,
+ DenomLossEventType,
+ DenomSelectionState,
+ DenominationInfo,
+ DenominationPubKey,
+ EddsaPublicKeyString,
+ EddsaSignatureString,
+ ExchangeAuditor,
+ ExchangeGlobalFees,
+ HashCodeString,
+ Logger,
RefreshReason,
- TalerErrorDetails,
- Timestamp,
+ TalerErrorDetail,
+ TalerPreciseTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ Transaction,
+ TransactionIdStr,
+ UnblindedSignature,
+ WireInfo,
+ WithdrawalExchangeAccountDetails,
+ codecForAny,
} from "@gnu-taler/taler-util";
-import { RetryInfo } from "./util/retries.js";
-import { PayCoinSelection } from "./util/coinSelection.js";
+import { DbRetryInfo, TaskIdentifiers } from "./common.js";
+import {
+ DbAccess,
+ DbAccessImpl,
+ DbReadOnlyTransaction,
+ 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
@@ -46,7 +120,7 @@ import { PayCoinSelection } from "./util/coinSelection.js";
* for all previous versions must be written, which should be
* avoided.
*/
-export const TALER_DB_NAME = "taler-wallet-main-v3";
+export const TALER_WALLET_MAIN_DB_NAME = "taler-wallet-main-v10";
/**
* Name of the metadata database. This database is used
@@ -54,8 +128,20 @@ export const TALER_DB_NAME = "taler-wallet-main-v3";
*
* (Minor migrations are handled via upgrade transactions.)
*/
-export const TALER_META_DB_NAME = "taler-wallet-meta";
+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";
/**
@@ -65,237 +151,290 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 1;
+export const WALLET_DB_MINOR_VERSION = 10;
-export enum ReserveRecordStatus {
- /**
- * Reserve must be registered with the bank.
- */
- REGISTERING_BANK = "registering-bank",
+declare const symDbProtocolTimestamp: unique symbol;
- /**
- * 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).
- */
- WAIT_CONFIRM_BANK = "wait-confirm-bank",
+declare const symDbPreciseTimestamp: unique symbol;
- /**
- * Querying reserve status with the exchange.
- */
- QUERYING_STATUS = "querying-status",
+/**
+ * Timestamp, stored as microseconds.
+ *
+ * Always rounded to a full second.
+ */
+export type DbProtocolTimestamp = number & { [symDbProtocolTimestamp]: true };
- /**
- * The corresponding withdraw record has been created.
- * No further processing is done, unless explicitly requested
- * by the user.
- */
- DORMANT = "dormant",
+/**
+ * Timestamp, stored as microseconds.
+ */
+export type DbPreciseTimestamp = number & { [symDbPreciseTimestamp]: true };
- /**
- * The bank aborted the withdrawal.
- */
- BANK_ABORTED = "bank-aborted",
+const DB_TIMESTAMP_FOREVER = Number.MAX_SAFE_INTEGER;
+
+export function timestampPreciseFromDb(
+ dbTs: DbPreciseTimestamp,
+): TalerPreciseTimestamp {
+ return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000));
}
-/**
- * Extra info about a reserve that is used
- * with a bank-integrated withdrawal.
- */
-export interface ReserveBankInfo {
- /**
- * Status URL that the wallet will use to query the status
- * of the Taler withdrawal operation on the bank's side.
- */
- statusUrl: string;
+export function timestampOptionalPreciseFromDb(
+ dbTs: DbPreciseTimestamp | undefined,
+): TalerPreciseTimestamp | undefined {
+ if (!dbTs) {
+ return undefined;
+ }
+ return TalerPreciseTimestamp.fromMilliseconds(Math.floor(dbTs / 1000));
+}
- /**
- * URL that the user can be redirected to, and allows
- * them to confirm (or abort) the bank-integrated withdrawal.
- */
- confirmUrl?: string;
+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;
+ }
+}
- /**
- * Exchange payto URI that the bank will use to fund the reserve.
- */
- exchangePaytoUri: string;
+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));
}
/**
- * A reserve record as stored in the wallet's database.
+ * 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=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
*/
-export interface ReserveRecord {
- /**
- * The reserve public key.
- */
- reservePub: string;
- /**
- * The reserve private key.
- */
- reservePriv: string;
+/**
+ * First possible operation status in the active range (inclusive).
+ */
+export const OPERATION_STATUS_ACTIVE_FIRST = 0x0100_0000;
+
+/**
+ * LAST possible operation status in the active range (inclusive).
+ */
+export const OPERATION_STATUS_ACTIVE_LAST = 0x0113_ffff;
+/**
+ * Status of a withdrawal.
+ */
+export enum WithdrawalGroupStatus {
/**
- * The exchange base URL.
+ * Reserve must be registered with the bank.
*/
- exchangeBaseUrl: string;
+ PendingRegisteringBank = 0x0100_0001,
+ SuspendedRegisteringBank = 0x0110_0001,
/**
- * Currency of the reserve.
+ * 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).
*/
- currency: string;
+ PendingWaitConfirmBank = 0x0100_0002,
+ SuspendedWaitConfirmBank = 0x0110_0002,
/**
- * Time when the reserve was created.
+ * Querying reserve status with the exchange.
*/
- timestampCreated: Timestamp;
+ PendingQueryingStatus = 0x0100_0003,
+ SuspendedQueryingStatus = 0x0110_0003,
/**
- * Time when the information about this reserve was posted to the bank.
- *
- * Only applies if bankWithdrawStatusUrl is defined.
- *
- * Set to 0 if that hasn't happened yet.
+ * Ready for withdrawal.
*/
- timestampReserveInfoPosted: Timestamp | undefined;
+ PendingReady = 0x0100_0004,
+ SuspendedReady = 0x0110_0004,
/**
- * Time when the reserve was confirmed by the bank.
- *
- * Set to undefined if not confirmed yet.
+ * Proposed to the user, has can choose to accept/refuse.
*/
- timestampBankConfirmed: Timestamp | undefined;
+ DialogProposed = 0x0101_0000,
/**
- * Wire information (as payto URI) for the bank account that
- * transferred funds for this reserve.
+ * We are telling the bank that we don't want to complete
+ * the withdrawal!
*/
- senderWire?: string;
+ AbortingBank = 0x0103_0001,
+ SuspendedAbortingBank = 0x0113_0001,
/**
- * Amount that was sent by the user to fund the reserve.
+ * Exchange wants KYC info from the user.
*/
- instructedAmount: AmountJson;
+ PendingKyc = 0x0100_0005,
+ SuspendedKyc = 0x0110_005,
/**
- * Extra state for when this is a withdrawal involving
- * a Taler-integrated bank.
+ * Exchange is doing AML checks.
*/
- bankInfo?: ReserveBankInfo;
-
- initialWithdrawalGroupId: string;
+ PendingAml = 0x0100_0006,
+ SuspendedAml = 0x0110_0006,
/**
- * Did we start the first withdrawal for this reserve?
- *
- * We only report a pending withdrawal for the reserve before
- * the first withdrawal has started.
+ * The corresponding withdraw record has been created.
+ * No further processing is done, unless explicitly requested
+ * by the user.
*/
- initialWithdrawalStarted: boolean;
+ Done = 0x0500_0000,
/**
- * Initial denomination selection, stored here so that
- * we can show this information in the transactions/balances
- * before we have a withdrawal group.
+ * The bank aborted the withdrawal.
*/
- initialDenomSel: DenomSelectionState;
+ FailedBankAborted = 0x0501_0001,
- reserveStatus: ReserveRecordStatus;
+ FailedAbortingBank = 0x0501_0002,
/**
- * Was a reserve query requested? If so, query again instead
- * of going into dormant status.
+ * Aborted in a state where we were supposed to
+ * talk to the exchange. Money might have been
+ * wired or not.
*/
- requestedQuery: boolean;
+ AbortedExchange = 0x0503_0001,
- /**
- * Time of the last successful status query.
- */
- lastSuccessfulStatusQuery: Timestamp | undefined;
+ AbortedBank = 0x0503_0002,
/**
- * Retry info. This field is present even if no retry is scheduled,
- * because we need it to be present for the index on the object store
- * to work.
+ * User didn't refused the withdrawal.
*/
- retryInfo: RetryInfo;
+ AbortedUserRefused = 0x0503_0003,
/**
- * Last error that happened in a reserve operation
- * (either talking to the bank or the exchange).
+ * Another wallet confirmed the withdrawal
+ * (by POSTing the reseve pub to the bank)
+ * before we had the chance.
+ *
+ * In this situation, we'll let the other wallet continue
+ * and give up ourselves.
*/
- lastError: TalerErrorDetails | undefined;
+ AbortedOtherWallet = 0x0503_0004,
}
/**
- * Record that indicates the wallet trusts
- * a particular auditor.
+ * Extra info about a withdrawal that is used
+ * with a bank-integrated withdrawal.
*/
-export interface AuditorTrustRecord {
+export interface ReserveBankInfo {
+ talerWithdrawUri: string;
+
/**
- * Currency that we trust this auditor for.
+ * URL that the user can be redirected to, and allows
+ * them to confirm (or abort) the bank-integrated withdrawal.
*/
- currency: string;
+ confirmUrl: string | undefined;
/**
- * Base URL of the auditor.
+ * Exchange payto URI that the bank will use to fund the reserve.
*/
- auditorBaseUrl: string;
+ exchangePaytoUri: string;
/**
- * Public key of the auditor.
+ * 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.
*/
- auditorPub: string;
+ timestampReserveInfoPosted: DbPreciseTimestamp | undefined;
/**
- * UIDs for the operation of adding this auditor
- * as a trusted auditor.
+ * Time when the reserve was confirmed by the bank.
+ *
+ * Set to undefined if not confirmed yet.
*/
- uids: string[];
+ timestampBankConfirmed: DbPreciseTimestamp | undefined;
}
/**
- * Record to indicate trust for a particular exchange.
+ * Status of a denomination.
*/
-export interface ExchangeTrustRecord {
+export enum DenominationVerificationStatus {
/**
- * Currency that we trust this exchange for.
+ * Verification was delayed (pending).
*/
- currency: string;
+ Unverified = 0x0100_0000,
/**
- * Canonicalized exchange base URL.
+ * Verified as valid.
*/
- exchangeBaseUrl: string;
+ VerifiedGood = 0x0500_0000,
/**
- * Master public key of the exchange.
+ * Verified as invalid.
*/
- exchangeMasterPub: string;
+ VerifiedBad = 0x0501_0000,
+}
+export interface DenomFees {
/**
- * UIDs for the operation of adding this exchange
- * as trusted.
+ * Fee for withdrawing.
*/
- uids: string[];
-}
+ feeWithdraw: AmountString;
-/**
- * Status of a denomination.
- */
-export enum DenominationVerificationStatus {
/**
- * Verification was delayed.
+ * Fee for depositing.
*/
- Unverified = "unverified",
+ feeDeposit: AmountString;
+
/**
- * Verified as valid.
+ * Fee for refreshing.
*/
- VerifiedGood = "verified-good",
+ feeRefresh: AmountString;
+
/**
- * Verified as invalid.
+ * Fee for refunding.
*/
- VerifiedBad = "verified-bad",
+ feeRefund: AmountString;
}
/**
@@ -303,14 +442,18 @@ export enum DenominationVerificationStatus {
*/
export interface DenominationRecord {
/**
- * Value of one coin of the denomination.
+ * Currency of the denomination.
+ *
+ * Stored separately as we have an index on it.
*/
- value: AmountJson;
+ currency: string;
+
+ value: AmountString;
/**
* The denomination public key.
*/
- denomPub: string;
+ denomPub: DenominationPubKey;
/**
* Hash of the denomination public key.
@@ -318,45 +461,27 @@ export interface DenominationRecord {
*/
denomPubHash: string;
- /**
- * Fee for withdrawing.
- */
- feeWithdraw: AmountJson;
-
- /**
- * Fee for depositing.
- */
- feeDeposit: AmountJson;
-
- /**
- * Fee for refreshing.
- */
- feeRefresh: AmountJson;
-
- /**
- * Fee for refunding.
- */
- feeRefund: AmountJson;
+ fees: DenomFees;
/**
* Validity start date of the denomination.
*/
- stampStart: Timestamp;
+ stampStart: DbProtocolTimestamp;
/**
* Date after which the currency can't be withdrawn anymore.
*/
- stampExpireWithdraw: Timestamp;
+ stampExpireWithdraw: DbProtocolTimestamp;
/**
* Date after the denomination officially doesn't exist anymore.
*/
- stampExpireLegal: Timestamp;
+ stampExpireLegal: DbProtocolTimestamp;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
- stampExpireDeposit: Timestamp;
+ stampExpireDeposit: DbProtocolTimestamp;
/**
* Signature by the exchange's master key over the denomination
@@ -384,6 +509,13 @@ export interface DenominationRecord {
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;
@@ -393,23 +525,47 @@ export interface DenominationRecord {
* on the denomination.
*/
exchangeMasterPub: string;
+}
+
+export namespace DenominationRecord {
+ export function toDenomInfo(d: DenominationRecord): DenominationInfo {
+ return {
+ denomPub: d.denomPub,
+ 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,
+ };
+ }
+}
+
+export interface ExchangeSignkeysRecord {
+ stampStart: DbProtocolTimestamp;
+ stampExpire: DbProtocolTimestamp;
+ stampEnd: DbProtocolTimestamp;
+ signkeyPub: EddsaPublicKeyString;
+ masterSig: EddsaSignatureString;
/**
- * Latest list issue date of the "/keys" response
- * that includes this denomination.
+ * Exchange details that thiis signkeys record belongs to.
*/
- listIssueDate: Timestamp;
+ exchangeDetailsRowId: number;
}
/**
- * Information about one of the exchange's bank accounts.
+ * Exchange details for a particular
+ * (exchangeBaseUrl, masterPublicKey, currency) tuple.
*/
-export interface ExchangeBankAccount {
- payto_uri: string;
- master_sig: string;
-}
-
export interface ExchangeDetailsRecord {
+ rowId?: number;
+
/**
* Master public key of the exchange.
*/
@@ -425,102 +581,123 @@ export interface ExchangeDetailsRecord {
/**
* Auditors (partially) auditing the exchange.
*/
- auditors: Auditor[];
+ auditors: ExchangeAuditor[];
/**
* Last observed protocol version.
*/
- protocolVersion: string;
-
- reserveClosingDelay: Duration;
-
- /**
- * Signing keys we got from the exchange, can also contain
- * older signing keys that are not returned by /keys anymore.
- *
- * FIXME: Should this be put into a separate object store?
- */
- signingKeys: ExchangeSignKeyJson[];
+ protocolVersionRange: string;
- /**
- * Terms of service text or undefined if not downloaded yet.
- *
- * This is just used as a cache of the last downloaded ToS.
- */
- termsOfServiceText: string | undefined;
+ reserveClosingDelay: TalerProtocolDuration;
/**
- * content-type of the last downloaded termsOfServiceText.
+ * Fees for exchange services
*/
- termsOfServiceContentType: string | undefined;
+ globalFees: ExchangeGlobalFees[];
- /**
- * ETag for last terms of service download.
- */
- termsOfServiceLastEtag: string | undefined;
+ wireInfo: WireInfo;
/**
- * ETag for last terms of service accepted.
+ * Age restrictions supported by the exchange (bitmask).
*/
- termsOfServiceAcceptedEtag: string | undefined;
-
- /**
- * Timestamp when the ToS was accepted.
- *
- * Used during backup merging.
- */
- termsOfServiceAcceptedTimestamp: Timestamp | undefined;
-
- wireInfo: WireInfo;
-}
-
-export interface WireInfo {
- feesForType: { [wireMethod: string]: WireFee[] };
-
- accounts: ExchangeBankAccount[];
+ ageMask?: number;
}
export interface ExchangeDetailsPointer {
masterPublicKey: string;
+
currency: string;
/**
* Timestamp when the (masterPublicKey, currency) pointer
* has been updated.
*/
- updateClock: Timestamp;
+ 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,
}
/**
* Exchange record as stored in the wallet's database.
*/
-export interface ExchangeRecord {
+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;
+
+ /**
+ * 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;
+
/**
- * Is this a permanent or temporary exchange record?
+ * If set to true, the next update to the exchange
+ * status will request /keys with no-cache headers set.
*/
- permanent: boolean;
+ cachebreakNextUpdate?: boolean;
+
+ /**
+ * Etag of the current ToS of the exchange.
+ */
+ tosCurrentEtag: string | undefined;
+
+ tosAcceptedEtag: string | undefined;
+
+ tosAcceptedTimestamp: DbPreciseTimestamp | undefined;
/**
- * Last time when the exchange was updated.
+ * Last time when the exchange /keys info was updated.
*/
- lastUpdate: Timestamp | undefined;
+ lastUpdate: DbPreciseTimestamp | undefined;
/**
* Next scheduled update for the exchange.
- *
- * (This field must always be present, so we can index on the timestamp.)
*/
- nextUpdate: Timestamp;
+ nextUpdateStamp: DbPreciseTimestamp;
+
+ updateRetryCounter?: number;
+
+ lastKeysEtag: string | undefined;
/**
* Next time that we should check if coins need to be refreshed.
@@ -528,14 +705,30 @@ export interface ExchangeRecord {
* Updated whenever the exchange's denominations are updated or when
* the refresh check has been done.
*/
- nextRefreshCheck: Timestamp;
+ nextRefreshCheckStamp: DbPreciseTimestamp;
- lastError?: TalerErrorDetails;
+ /**
+ * Public key of the reserve that we're currently using for
+ * receiving P2P payments.
+ */
+ currentMergeReserveRowId?: number;
+
+ /**
+ * Defaults to false.
+ */
+ peerPaymentsDisabled?: boolean;
/**
- * Retry status for fetching updated information about the exchange.
+ * Defaults to false.
*/
- retryInfo: RetryInfo;
+ noFees?: boolean;
+}
+
+export enum PlanchetStatus {
+ Pending = 0x0100_0000,
+ KycRequired = 0x0100_0001,
+ WithdrawalDone = 0x0500_000,
+ AbortedReplaced = 0x0503_0001,
}
/**
@@ -563,54 +756,27 @@ export interface PlanchetRecord {
*/
coinIdx: number;
- withdrawalDone: boolean;
+ planchetStatus: PlanchetStatus;
- lastError: TalerErrorDetails | undefined;
-
- /**
- * Public key of the reserve that this planchet
- * is being withdrawn from.
- *
- * Can be the empty string (non-null/undefined for DB indexing)
- * if this is a tipping reserve.
- */
- reservePub: string;
+ lastError: TalerErrorDetail | undefined;
denomPubHash: string;
- denomPub: string;
-
blindingKey: string;
withdrawSig: string;
- coinEv: string;
+ coinEv: CoinEnvelope;
coinEvHash: string;
- coinValue: AmountJson;
-
- isFromTip: boolean;
-}
-
-/**
- * Status of a coin.
- */
-export enum CoinStatus {
- /**
- * Withdrawn and never shown to anybody.
- */
- Fresh = "fresh",
- /**
- * A coin that has been spent and refreshed.
- */
- Dormant = "dormant",
+ ageCommitmentProof?: AgeCommitmentProof;
}
export enum CoinSourceType {
Withdraw = "withdraw",
Refresh = "refresh",
- Tip = "tip",
+ Reward = "reward",
}
export interface WithdrawCoinSource {
@@ -634,16 +800,20 @@ export interface WithdrawCoinSource {
export interface RefreshCoinSource {
type: CoinSourceType.Refresh;
+ refreshGroupId: string;
oldCoinPub: string;
}
-export interface TipCoinSource {
- type: CoinSourceType.Tip;
- walletTipId: string;
+export interface RewardCoinSource {
+ type: CoinSourceType.Reward;
+ walletRewardId: string;
coinIndex: number;
}
-export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
+export type CoinSource =
+ | WithdrawCoinSource
+ | RefreshCoinSource
+ | RewardCoinSource;
/**
* CoinRecord as stored in the "coins" data store
@@ -656,6 +826,14 @@ export interface CoinRecord {
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;
@@ -666,11 +844,6 @@ export interface CoinRecord {
coinPriv: string;
/**
- * Key used by the exchange used to sign the coin.
- */
- denomPub: string;
-
- /**
* Hash of the public key that signs the coin.
*/
denomPubHash: string;
@@ -678,12 +851,7 @@ export interface CoinRecord {
/**
* Unblinded signature by the exchange.
*/
- denomSig: string;
-
- /**
- * Amount that's left on the coin.
- */
- currentAmount: AmountJson;
+ denomSig: UnblindedSignature;
/**
* Base URL that identifies the exchange from which we got the
@@ -692,11 +860,6 @@ export interface CoinRecord {
exchangeBaseUrl: string;
/**
- * The coin is currently suspended, and will not be used for payments.
- */
- suspended: boolean;
-
- /**
* Blinding key used when withdrawing the coin.
* Potentionally used again during payback.
*/
@@ -716,130 +879,67 @@ export interface CoinRecord {
status: CoinStatus;
/**
- * Information about what the coin has been allocated for.
- * Used to prevent allocation of the same coin for two different payments.
+ * Non-zero for visible.
+ *
+ * A coin is visible when it is fresh and the
+ * source transaction is in a final state.
*/
- allocation?: CoinAllocation;
-}
+ visible?: number;
-export interface CoinAllocation {
- id: string;
- amount: AmountString;
-}
-
-export enum ProposalStatus {
- /**
- * Not downloaded yet.
- */
- DOWNLOADING = "downloading",
- /**
- * Proposal downloaded, but the user needs to accept/reject it.
- */
- PROPOSED = "proposed",
/**
- * The user has accepted the proposal.
- */
- ACCEPTED = "accepted",
- /**
- * The user has rejected the proposal.
- */
- REFUSED = "refused",
- /**
- * Downloading or processing the proposal has failed permanently.
- */
- PERMANENTLY_FAILED = "permanently-failed",
- /**
- * Downloaded proposal was detected as a re-purchase.
+ * Information about what the coin has been allocated for.
+ *
+ * Used for:
+ * - Diagnostics
+ * - Idempotency of applying a coin selection (e.g. after re-selection)
*/
- REPURCHASE = "repurchase",
-}
+ spendAllocation: CoinAllocation | undefined;
-export interface ProposalDownload {
/**
- * The contract that was offered by the merchant.
+ * Maximum age of purchases that can be made with this coin.
+ *
+ * (Used for indexing, redundant with {@link ageCommitmentProof}).
*/
- contractTermsRaw: any;
+ maxAge: number;
- contractData: WalletContractData;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
}
/**
- * Record for a downloaded order, stored in the wallet's database.
+ * Coin allocation, i.e. what a coin has been used for.
*/
-export interface ProposalRecord {
- orderId: string;
-
- merchantBaseUrl: string;
-
- /**
- * Downloaded data from the merchant.
- */
- download: ProposalDownload | undefined;
-
- /**
- * Unique ID when the order is stored in the wallet DB.
- */
- proposalId: string;
-
- /**
- * Timestamp (in ms) of when the record
- * was created.
- */
- timestamp: Timestamp;
-
- /**
- * Private key for the nonce.
- */
- noncePriv: string;
-
- /**
- * Public key for the nonce.
- */
- noncePub: string;
-
- claimToken: string | undefined;
-
- proposalStatus: ProposalStatus;
-
- repurchaseProposalId: string | undefined;
-
- /**
- * Session ID we got when downloading the contract.
- */
- downloadSessionId?: string;
-
+export interface CoinAllocation {
/**
- * Retry info, even present when the operation isn't active to allow indexing
- * on the next retry timestamp.
+ * ID of the allocation, should be the ID of the transaction that
*/
- retryInfo?: RetryInfo;
-
- lastError: TalerErrorDetails | undefined;
+ id: TransactionIdStr;
+ amount: AmountString;
}
/**
- * Status of a tip we got from a merchant.
+ * Status of a reward we got from a merchant.
*/
-export interface TipRecord {
- lastError: TalerErrorDetails | undefined;
-
+export interface RewardRecord {
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used.
*/
- acceptedTimestamp: Timestamp | undefined;
+ acceptedTimestamp: DbPreciseTimestamp | undefined;
/**
* The tipped amount.
*/
- tipAmountRaw: AmountJson;
+ rewardAmountRaw: AmountString;
- tipAmountEffective: AmountJson;
+ /**
+ * Effect on the balance (including fees etc).
+ */
+ rewardAmountEffective: AmountString;
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
*/
- tipExpiration: Timestamp;
+ rewardExpiration: DbProtocolTimestamp;
/**
* The exchange that will sign our coins, chosen by the merchant.
@@ -854,6 +954,9 @@ export interface TipRecord {
/**
* Denomination selection made by the wallet for picking up
* this tip.
+ *
+ * FIXME: Put this into some DenomSelectionCacheRecord instead of
+ * storing it here!
*/
denomsSel: DenomSelectionState;
@@ -862,7 +965,7 @@ export interface TipRecord {
/**
* Tip ID chosen by the wallet.
*/
- walletTipId: string;
+ walletRewardId: string;
/**
* Secret seed used to derive planchets for this tip.
@@ -870,46 +973,83 @@ export interface TipRecord {
secretSeed: string;
/**
- * The merchant's identifier for this tip.
+ * The merchant's identifier for this reward.
*/
- merchantTipId: string;
+ merchantRewardId: string;
- createdTimestamp: Timestamp;
+ createdTimestamp: DbPreciseTimestamp;
/**
- * Timestamp for when the wallet finished picking up the tip
- * from the merchant.
+ * The url to be redirected after the tip is accepted.
*/
- pickedUpTimestamp: Timestamp | undefined;
+ next_url: string | undefined;
/**
- * Retry info, even present when the operation isn't active to allow indexing
- * on the next retry timestamp.
+ * Timestamp for when the wallet finished picking up the tip
+ * from the merchant.
*/
- retryInfo: RetryInfo;
+ pickedUpTimestamp: DbPreciseTimestamp | undefined;
+
+ status: RewardRecordStatus;
+}
+
+export enum RewardRecordStatus {
+ PendingPickup = 0x0100_0000,
+ SuspendedPickup = 0x0110_0000,
+ DialogAccept = 0x0101_0000,
+ Done = 0x0500_0000,
+ Aborted = 0x0500_0000,
+ Failed = 0x0501_000,
}
export enum RefreshCoinStatus {
- Pending = "pending",
- Finished = "finished",
+ Pending = 0x0100_0000,
+ Finished = 0x0500_0000,
/**
* The refresh for this coin has been frozen, because of a permanent error.
* More info in lastErrorPerCoin.
*/
- Frozen = "frozen",
+ Failed = 0x0501_000,
}
-export interface RefreshGroupRecord {
+export enum RefreshOperationStatus {
+ Pending = 0x0100_0000,
+ Suspended = 0x0110_0000,
+
+ Finished = 0x0500_000,
+ Failed = 0x0501_000,
+}
+
+/**
+ * Status of a single element of a deposit group.
+ */
+export enum DepositElementStatus {
+ DepositPending = 0x0100_0000,
/**
- * Retry info, even present when the operation isn't active to allow indexing
- * on the next retry timestamp.
+ * Accepted, but tracking.
*/
- retryInfo: RetryInfo;
+ Tracking = 0x0100_0001,
+ KycRequired = 0x0100_0002,
+ Wired = 0x0500_0000,
+ RefundSuccess = 0x0503_0000,
+ RefundFailed = 0x0501_0000,
+}
- lastError: TalerErrorDetails | undefined;
+export interface RefreshGroupPerExchangeInfo {
+ /**
+ * (Expected) output once the refresh group succeeded.
+ */
+ outputEffective: AmountString;
+}
- lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetails };
+/**
+ * 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
@@ -918,19 +1058,24 @@ export interface RefreshGroupRecord {
refreshGroupId: string;
/**
+ * Currency of this refresh group.
+ */
+ currency: string;
+
+ /**
* Reason why this refresh group has been created.
*/
reason: RefreshReason;
+ originatingTransactionId?: string;
+
oldCoinPubs: string[];
- // FIXME: Should this go into a separate
- // object store for faster updates?
- refreshSessionPerCoin: (RefreshSessionRecord | undefined)[];
+ inputPerCoin: AmountString[];
- inputPerCoin: AmountJson[];
+ expectedOutputPerCoin: AmountString[];
- estimatedOutputPerCoin: AmountJson[];
+ infoPerExchange?: Record<string, RefreshGroupPerExchangeInfo>;
/**
* Flag for each coin whether refreshing finished.
@@ -940,23 +1085,25 @@ export interface RefreshGroupRecord {
*/
statusPerCoin: RefreshCoinStatus[];
- timestampCreated: Timestamp;
+ timestampCreated: DbPreciseTimestamp;
/**
* Timestamp when the refresh session finished.
*/
- timestampFinished: Timestamp | undefined;
-
- /**
- * No coins are pending, but at least one is frozen.
- */
- frozen?: boolean;
+ timestampFinished: DbPreciseTimestamp | undefined;
}
/**
* Ongoing refresh
*/
export interface RefreshSessionRecord {
+ refreshGroupId: string;
+
+ /**
+ * Index of the coin in the refresh group.
+ */
+ coinIndex: number;
+
/**
* 512-bit secret that can be used to derive
* the other cryptographic material for the refresh session.
@@ -967,7 +1114,7 @@ export interface RefreshSessionRecord {
* Sum of the value of denominations we want
* to withdraw in this session, without fees.
*/
- amountRefreshOutput: AmountJson;
+ amountRefreshOutput: AmountString;
/**
* Hashed denominations of the newly requested coins.
@@ -981,173 +1128,178 @@ export interface RefreshSessionRecord {
* The no-reveal-index after we've done the melting.
*/
norevealIndex?: number;
+
+ lastError?: TalerErrorDetail;
}
-/**
- * Wire fee for one wire method as stored in the
- * wallet's database.
- */
-export interface WireFee {
+export enum RefundReason {
/**
- * Fee for wire transfers.
+ * Normal refund given by the merchant.
*/
- wireFee: AmountJson;
-
+ NormalRefund = "normal-refund",
/**
- * Fees to close and refund a reserve.
+ * Refund from an aborted payment.
*/
- closingFee: AmountJson;
+ AbortRefund = "abort-pay-refund",
+}
+export enum PurchaseStatus {
/**
- * Start date of the fee.
+ * Not downloaded yet.
*/
- startStamp: Timestamp;
+ PendingDownloadingProposal = 0x0100_0000,
+ SuspendedDownloadingProposal = 0x0110_0000,
/**
- * End date of the fee.
+ * The user has accepted the proposal.
*/
- endStamp: Timestamp;
+ PendingPaying = 0x0100_0001,
+ SuspendedPaying = 0x0110_0001,
/**
- * Signature made by the exchange master key.
+ * Currently in the process of aborting with a refund.
*/
- sig: string;
-}
+ AbortingWithRefund = 0x0103_0000,
+ SuspendedAbortingWithRefund = 0x0113_0000,
-export enum RefundState {
- Failed = "failed",
- Applied = "applied",
- Pending = "pending",
-}
-
-/**
- * State of one refund from the merchant, maintained by the wallet.
- */
-export type WalletRefundItem =
- | WalletRefundFailedItem
- | WalletRefundPendingItem
- | WalletRefundAppliedItem;
+ /**
+ * Paying a second time, likely with different session ID
+ */
+ PendingPayingReplay = 0x0100_0002,
+ SuspendedPayingReplay = 0x0110_0002,
-export interface WalletRefundItemCommon {
- // Execution time as claimed by the merchant
- executionTime: Timestamp;
+ /**
+ * Query for refunds (until query succeeds).
+ */
+ PendingQueryingRefund = 0x0100_0003,
+ SuspendedQueryingRefund = 0x0110_0003,
/**
- * Time when the wallet became aware of the refund.
+ * Query for refund (until auto-refund deadline is reached).
*/
- obtainedTime: Timestamp;
+ PendingQueryingAutoRefund = 0x0100_0004,
+ SuspendedQueryingAutoRefund = 0x0110_0004,
- refundAmount: AmountJson;
+ PendingAcceptRefund = 0x0100_0005,
+ SuspendedPendingAcceptRefund = 0x0110_0005,
- refundFee: AmountJson;
+ /**
+ * Proposal downloaded, but the user needs to accept/reject it.
+ */
+ DialogProposed = 0x0101_0000,
/**
- * Upper bound on the refresh cost incurred by
- * applying this refund.
- *
- * Might be lower in practice when two refunds on the same
- * coin are refreshed in the same refresh operation.
+ * Proposal shared to other wallet or read from other wallet
+ * the user needs to accept/reject it.
*/
- totalRefreshCostBound: AmountJson;
+ DialogShared = 0x0101_0001,
- coinPub: string;
+ /**
+ * The user has rejected the proposal.
+ */
+ AbortedProposalRefused = 0x0503_0000,
- rtransactionId: number;
-}
+ /**
+ * Downloading or processing the proposal has failed permanently.
+ */
+ FailedClaim = 0x0501_0000,
-/**
- * Failed refund, either because the merchant did
- * something wrong or it expired.
- */
-export interface WalletRefundFailedItem extends WalletRefundItemCommon {
- type: RefundState.Failed;
-}
+ /**
+ * Tried to abort, but aborting failed or was cancelled.
+ */
+ FailedAbort = 0x0501_0001,
-export interface WalletRefundPendingItem extends WalletRefundItemCommon {
- type: RefundState.Pending;
-}
+ FailedPaidByOther = 0x0501_0002,
-export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
- type: RefundState.Applied;
-}
+ /**
+ * Payment was successful.
+ */
+ Done = 0x0500_0000,
-export enum RefundReason {
/**
- * Normal refund given by the merchant.
+ * Downloaded proposal was detected as a re-purchase.
*/
- NormalRefund = "normal-refund",
+ DoneRepurchaseDetected = 0x0500_0001,
+
/**
- * Refund from an aborted payment.
+ * The payment has been aborted.
*/
- AbortRefund = "abort-pay-refund",
-}
+ AbortedIncompletePayment = 0x0503_0000,
-export interface AllowedAuditorInfo {
- auditorBaseUrl: string;
- auditorPub: string;
-}
+ AbortedRefunded = 0x0503_0001,
-export interface AllowedExchangeInfo {
- exchangeBaseUrl: string;
- exchangePub: string;
+ AbortedOrderDeleted = 0x0503_0002,
}
/**
- * Data extracted from the contract terms that is relevant for payment
- * processing in the wallet.
+ * Partial information about the downloaded proposal.
+ * Only contains data that is relevant for indexing on the
+ * "purchases" object stores.
*/
-export interface WalletContractData {
- products?: Product[];
- summaryI18n: { [lang_tag: string]: string } | undefined;
+export interface ProposalDownloadInfo {
+ contractTermsHash: string;
+ fulfillmentUrl?: string;
+ currency: string;
+ contractTermsMerchantSig: string;
+}
+export interface DbCoinSelection {
+ coinPubs: string[];
+ coinContributions: AmountString[];
+}
+
+export interface PurchasePayInfo {
/**
- * Fulfillment URL, or the empty string if the order has no fulfillment URL.
- *
- * Stored as a non-nullable string as we use this field for IndexedDB indexing.
+ * Undefined if payment is blocked by a pending refund.
*/
- fulfillmentUrl: string;
-
- contractTermsHash: string;
- fulfillmentMessage?: string;
- fulfillmentMessageI18n?: InternationalizedString;
- merchantSig: string;
- merchantPub: string;
- merchant: MerchantInfo;
- amount: AmountJson;
- orderId: string;
- merchantBaseUrl: string;
- summary: string;
- autoRefund: Duration | undefined;
- maxWireFee: AmountJson;
- wireFeeAmortization: number;
- payDeadline: Timestamp;
- refundDeadline: Timestamp;
- allowedAuditors: AllowedAuditorInfo[];
- allowedExchanges: AllowedExchangeInfo[];
- timestamp: Timestamp;
- wireMethod: string;
- wireInfoHash: string;
- maxDepositFee: AmountJson;
-}
-
-export enum AbortStatus {
- None = "none",
- AbortRefund = "abort-refund",
- AbortFinished = "abort-finished",
+ payCoinSelection?: DbCoinSelection;
+ /**
+ * Undefined if payment is blocked by a pending refund.
+ */
+ payCoinSelectionUid?: string;
+ 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;
+
+ /**
* Private key for the nonce.
*/
noncePriv: string;
@@ -1159,22 +1311,10 @@ export interface PurchaseRecord {
/**
* Downloaded and parsed proposal data.
- *
- * FIXME: Move this into another object store,
- * to improve read/write perf on purchases.
*/
- download: ProposalDownload;
+ download: ProposalDownloadInfo | undefined;
- /**
- * Deposit permissions, available once the user has accepted the payment.
- *
- * This value is cached and derived from payCoinSelection.
- */
- coinDepositPermissions: CoinDepositPermission[] | undefined;
-
- payCoinSelection: PayCoinSelection;
-
- payCoinSelectionUid: string;
+ payInfo: PurchasePayInfo | undefined;
/**
* Pending removals from pay coin selection.
@@ -1186,79 +1326,65 @@ export interface PurchaseRecord {
*/
pendingRemovedCoinPubs?: string[];
- totalPayCost: AmountJson;
-
/**
* Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful.
*/
- timestampFirstSuccessfulPay: Timestamp | undefined;
+ timestampFirstSuccessfulPay: DbPreciseTimestamp | undefined;
merchantPaySig: string | undefined;
- /**
- * When was the purchase made?
- * Refers to the time that the user accepted.
- */
- timestampAccept: Timestamp;
+ posConfirmation: string | undefined;
/**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- */
- refunds: { [refundKey: string]: WalletRefundItem };
-
- /**
- * When was the last refund made?
- * Set to 0 if no refund was made on the purchase.
+ * This purchase was created by reading
+ * a payment share or the wallet
+ * the nonce public by a payment share
*/
- timestampLastRefundStatus: Timestamp | undefined;
+ shared: boolean;
/**
- * Last session signature that we submitted to /pay (if any).
+ * When was the purchase record created?
*/
- lastSessionId: string | undefined;
+ timestamp: DbPreciseTimestamp;
/**
- * Set for the first payment, or on re-plays.
+ * When was the purchase made?
+ * Refers to the time that the user accepted.
*/
- paymentSubmitPending: boolean;
+ timestampAccept: DbPreciseTimestamp | undefined;
/**
- * Do we need to query the merchant for the refund status
- * of the payment?
- */
- refundQueryRequested: boolean;
-
- abortStatus: AbortStatus;
-
- payRetryInfo?: RetryInfo;
-
- lastPayError: TalerErrorDetails | undefined;
-
- /**
- * Retry information for querying the refund status with the merchant.
+ * When was the last refund made?
+ * Set to 0 if no refund was made on the purchase.
*/
- refundStatusRetryInfo: RetryInfo;
+ timestampLastRefundStatus: DbPreciseTimestamp | undefined;
/**
- * Last error (or undefined) for querying the refund status with the merchant.
+ * Last session signature that we submitted to /pay (if any).
*/
- lastRefundStatusError: TalerErrorDetails | undefined;
+ lastSessionId: string | undefined;
/**
* Continue querying the refund status until this deadline has expired.
*/
- autoRefundDeadline: Timestamp | undefined;
+ autoRefundDeadline: DbProtocolTimestamp | undefined;
/**
- * Is the payment frozen? I.e. did we encounter
- * an error where it doesn't make sense to retry.
+ * How much merchant has refund to be taken but the wallet
+ * did not picked up yet
*/
- payFrozen?: boolean;
+ refundAmountAwaiting: AmountString | undefined;
}
-export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
+export enum ConfigRecordKey {
+ WalletBackupState = "walletBackupState",
+ CurrencyDefaultsApplied = "currencyDefaultsApplied",
+ DevMode = "devMode",
+ // Only for testing, do not use!
+ TestLoopTx = "testTxLoop",
+ LastInitInfo = "lastInitInfo",
+}
/**
* Configuration key/value entries to configure
@@ -1266,10 +1392,12 @@ export const WALLET_BACKUP_STATE_KEY = "walletBackupState";
*/
export type ConfigRecord =
| {
- key: typeof WALLET_BACKUP_STATE_KEY;
+ key: ConfigRecordKey.WalletBackupState;
value: WalletBackupConfState;
}
- | { key: "currencyDefaultsApplied"; value: boolean };
+ | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean }
+ | { key: ConfigRecordKey.TestLoopTx; value: number }
+ | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp };
export interface WalletBackupConfState {
deviceId: string;
@@ -1284,73 +1412,196 @@ export interface WalletBackupConfState {
/**
* Timestamp stored in the last backup.
*/
- lastBackupTimestamp?: Timestamp;
+ lastBackupTimestamp?: DbPreciseTimestamp;
/**
* Last time we tried to do a backup.
*/
- lastBackupCheckTimestamp?: Timestamp;
+ lastBackupCheckTimestamp?: DbPreciseTimestamp;
lastBackupNonce?: string;
}
-/**
- * Selected denominations withn some extra info.
- */
-export interface DenomSelectionState {
- totalCoinValue: AmountJson;
- totalWithdrawCost: AmountJson;
- selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[];
+// 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";
+
+export interface KycPendingInfo {
+ paytoHash: string;
+ requirementRow: number;
+}
/**
* Group of withdrawal operations that need to be executed.
- * (Either for a normal withdrawal or from a tip.)
+ * (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;
+
+ kycPending?: KycPendingInfo;
+
+ kycUrl?: string;
+
/**
* 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: Timestamp;
+ timestampStart: DbPreciseTimestamp;
/**
* When was the withdrawal operation completed?
*/
- timestampFinish?: Timestamp;
+ timestampFinish?: DbPreciseTimestamp;
+
+ /**
+ * Current status of the reserve.
+ */
+ status: WithdrawalGroupStatus;
+
+ /**
+ * Wire information (as payto URI) for the bank account that
+ * transferred funds for this reserve.
+ *
+ * FIXME: Doesn't this belong to the bankAccounts object store?
+ */
+ senderWire?: string;
+
+ /**
+ * 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: AmountJson;
+ rawWithdrawalAmount: AmountString;
- denomsSel: DenomSelectionState;
-
- denomSelUid: string;
+ /**
+ * 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;
/**
- * Retry info, always present even on completed operations so that indexing works.
+ * Denominations selected for withdrawal.
*/
- retryInfo: RetryInfo;
+ denomsSel: DenomSelectionState;
- lastError: TalerErrorDetails | undefined;
+ /**
+ * UID of the denomination selection.
+ *
+ * Used for merging backups.
+ *
+ * FIXME: Should this not also include a timestamp for more logical merging?
+ */
+ denomSelUid: string;
}
export interface BankWithdrawUriRecord {
@@ -1365,6 +1616,14 @@ export interface BankWithdrawUriRecord {
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.
*
@@ -1377,9 +1636,13 @@ export interface RecoupGroupRecord {
*/
recoupGroupId: string;
- timestampStarted: Timestamp;
+ exchangeBaseUrl: string;
+
+ operationStatus: RecoupOperationStatus;
- timestampFinished: Timestamp | undefined;
+ timestampStarted: DbPreciseTimestamp;
+
+ timestampFinished: DbPreciseTimestamp | undefined;
/**
* Public keys that identify the coins being recouped
@@ -1395,27 +1658,10 @@ export interface RecoupGroupRecord {
recoupFinishedPerCoin: boolean[];
/**
- * We store old amount (i.e. before recoup) of recouped coins here,
- * as the balance of a recouped coin is set to zero when the
- * recoup group is created.
- */
- oldAmountPerCoin: AmountJson[];
-
- /**
* Public keys of coins that should be scheduled for refreshing
* after all individual recoups are done.
*/
- scheduleRefreshCoins: string[];
-
- /**
- * Retry info.
- */
- retryInfo: RetryInfo;
-
- /**
- * Last error that occurred, if any.
- */
- lastError: TalerErrorDetails | undefined;
+ scheduleRefreshCoins: CoinRefreshRequest[];
}
export enum BackupProviderStateTag {
@@ -1430,20 +1676,12 @@ export type BackupProviderState =
}
| {
tag: BackupProviderStateTag.Ready;
- nextBackupTimestamp: Timestamp;
+ nextBackupTimestamp: DbPreciseTimestamp;
}
| {
tag: BackupProviderStateTag.Retrying;
- retryInfo: RetryInfo;
- lastError?: TalerErrorDetails;
};
-export interface BackupProviderTerms {
- supportedProtocolVersion: string;
- annualFee: AmountString;
- storageLimitInMegabytes: number;
-}
-
export interface BackupProviderRecord {
/**
* Base URL of the provider.
@@ -1477,7 +1715,7 @@ export interface BackupProviderRecord {
* Does NOT correspond to the timestamp of the backup,
* which only changes when the backup content changes.
*/
- lastBackupCycleTimestamp?: Timestamp;
+ lastBackupCycleTimestamp?: DbPreciseTimestamp;
/**
* Proposal that we're currently trying to pay for.
@@ -1488,6 +1726,8 @@ export interface BackupProviderRecord {
*/
currentPaymentProposalId?: string;
+ shouldRetryFreshProposal: boolean;
+
/**
* Proposals that were used to pay (or attempt to pay) the provider.
*
@@ -1504,12 +1744,60 @@ export interface BackupProviderRecord {
uids: string[];
}
+export enum DepositOperationStatus {
+ PendingDeposit = 0x0100_0000,
+ PendingTrack = 0x0100_0001,
+ PendingKyc = 0x0100_0002,
+
+ Aborting = 0x0103_0000,
+
+ SuspendedDeposit = 0x0110_0000,
+ SuspendedTrack = 0x0110_0001,
+ SuspendedKyc = 0x0110_0002,
+
+ SuspendedAborting = 0x0113_0000,
+
+ Finished = 0x0500_0000,
+ Failed = 0x0501_0000,
+ Aborted = 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;
@@ -1525,106 +1813,729 @@ export interface DepositGroupRecord {
salt: string;
};
+ contractTermsHash: string;
+
+ payCoinSelection?: DbCoinSelection;
+
+ payCoinSelectionUid?: string;
+
+ totalPayCost: AmountString;
+
+ /**
+ * The counterparty effective deposit amount.
+ */
+ counterpartyEffectiveDepositAmount: AmountString;
+
+ timestampCreated: DbPreciseTimestamp;
+
+ timestampFinished: 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;
+
+ kycInfo?: DepositKycInfo;
+
+ // FIXME: Do we need this and should it be in this object store?
+ trackingState?: {
+ [signature: string]: DepositTrackingInfo;
+ };
+}
+
+export interface DepositKycInfo {
+ kycUrl: string;
+ requirementRow: number;
+ paytoHash: string;
+ exchangeBaseUrl: string;
+}
+
+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,
+ /**
+ * Refresh after the purse got deleted by the wallet.
+ */
+ AbortingRefreshDeleted = 0x0103_0001,
+ /**
+ * Refresh after the purse expired.
+ */
+ AbortingRefreshExpired = 0x0103_0002,
+
+ SuspendedCreatePurse = 0x0110_0000,
+ SuspendedReady = 0x0110_0001,
+ SuspendedAbortingDeletePurse = 0x0113_0000,
+ SuspendedAbortingRefreshDeleted = 0x0113_0001,
+ SuspendedAbortingRefreshExpired = 0x0113_0002,
+
+ Done = 0x0500_0000,
+ Aborted = 0x0503_0000,
+ Failed = 0x0501_0000,
+ Expired = 0x0502_0000,
+}
+
+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;
+
+ /**
+ * Instructed amount.
+ */
+ amount: AmountString;
+
+ 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;
+
/**
- * Verbatim contract terms.
+ * Public key of the merge capability of the purse.
*/
- contractTermsRaw: ContractTerms;
+ 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;
+
+ /**
+ * Status of the peer push payment initiation.
+ */
+ status: PeerPushDebitStatus;
+}
+
+export enum PeerPullPaymentCreditStatus {
+ PendingCreatePurse = 0x0100_0000,
+ /**
+ * Purse created, waiting for the other party to accept the
+ * invoice and deposit money into it.
+ */
+ PendingReady = 0x0100_0001,
+ PendingMergeKycRequired = 0x0100_0002,
+ PendingWithdrawing = 0x0100_0003,
+
+ AbortingDeletePurse = 0x0103_0000,
+
+ SuspendedCreatePurse = 0x0110_0000,
+ SuspendedReady = 0x0110_0001,
+ SuspendedMergeKycRequired = 0x0110_0002,
+ SuspendedWithdrawing = 0x0110_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;
- payCoinSelection: PayCoinSelection;
+ mergePub: string;
+ mergePriv: string;
- payCoinSelectionUid: string;
+ contractPub: string;
+ contractPriv: string;
- totalPayCost: AmountJson;
+ contractEncNonce: string;
- effectiveDepositAmount: AmountJson;
+ mergeTimestamp: DbPreciseTimestamp;
- depositedPerCoin: boolean[];
+ mergeReserveRowId: number;
- timestampCreated: Timestamp;
+ /**
+ * Status of the peer pull payment initiation.
+ */
+ status: PeerPullPaymentCreditStatus;
- timestampFinished: Timestamp | undefined;
+ kycInfo?: KycPendingInfo;
- lastError: TalerErrorDetails | undefined;
+ kycUrl?: string;
+
+ withdrawalGroupId: string | undefined;
+}
+export enum PeerPushCreditStatus {
+ PendingMerge = 0x0100_0000,
+ PendingMergeKycRequired = 0x0100_0001,
/**
- * Retry info.
+ * Merge was successful and withdrawal group has been created, now
+ * everything is in the hand of the withdrawal group.
*/
- retryInfo?: RetryInfo;
+ PendingWithdrawing = 0x0100_0002,
+
+ SuspendedMerge = 0x0110_0000,
+ SuspendedMergeKycRequired = 0x0110_0001,
+ SuspendedWithdrawing = 0x0110_0002,
+
+ DialogProposed = 0x0101_0000,
+
+ Done = 0x0500_0000,
+ Aborted = 0x0503_0000,
+ Failed = 0x0501_0000,
}
/**
- * Record for a deposits that the wallet observed
- * as a result of double spending, but which is not
- * present in the wallet's own database otherwise.
+ * Record for a push P2P payment that this wallet was offered.
+ *
+ * Unique: (exchangeBaseUrl, pursePub)
*/
-export interface GhostDepositGroupRecord {
+export interface PeerPushPaymentIncomingRecord {
+ 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;
+
+ /**
+ * 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;
+
+ kycInfo?: KycPendingInfo;
+
+ kycUrl?: string;
+}
+
+export enum PeerPullDebitRecordStatus {
+ PendingDeposit = 0x0100_0001,
+ AbortingRefresh = 0x0103_0001,
+
+ SuspendedDeposit = 0x0110_0001,
+ SuspendedAbortingRefresh = 0x0113_0001,
+
+ DialogProposed = 0x0101_0001,
+
+ Done = 0x0500_0000,
+ Aborted = 0x0503_0000,
+ Failed = 0x0501_0000,
+}
+
+export interface PeerPullPaymentCoinSelection {
+ contributions: AmountString[];
+ coinPubs: CoinPublicKeyString[];
+
/**
- * When multiple deposits for the same contract terms hash
- * have a different timestamp, we choose the earliest one.
+ * Total cost based on the coin selection.
+ * Non undefined after status === "Accepted"
*/
- timestamp: Timestamp;
+ totalCost: AmountString | undefined;
+}
+
+/**
+ * AKA PeerPullDebit.
+ */
+export interface PeerPullPaymentIncomingRecord {
+ peerPullDebitId: string;
+
+ pursePub: string;
+
+ exchangeBaseUrl: string;
+
+ amount: AmountString;
contractTermsHash: string;
- deposits: {
- coinPub: string;
- amount: AmountString;
- timestamp: Timestamp;
- depositFee: AmountString;
- merchantPub: string;
- coinSig: string;
- wireHash: 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;
+
+ coinSel?: PeerPullPaymentCoinSelection;
}
-export interface TombstoneRecord {
+/**
+ * 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.
+ */
+export interface ReserveRecord {
+ rowId?: number;
+ reservePub: string;
+ reservePriv: string;
+}
+
+export interface OperationRetryRecord {
/**
- * Tombstone ID, with the syntax "<type>:<key>".
+ * 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;
+
+ /**
+ * 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 UserAttentionRecord {
+ info: AttentionInfo;
+
+ entityId: string;
+
+ /**
+ * When the notification was created.
+ */
+ created: DbPreciseTimestamp;
+
+ /**
+ * When the user mark this notification as read.
+ */
+ read: DbPreciseTimestamp | undefined;
+}
+
+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;
+
+ 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;
+}
+
+/**
+ * Primary key: transactionItem.transactionId
+ */
+export interface TransactionRecord {
+ /**
+ * Transaction item returned to the client.
+ */
+ transactionItem: Transaction;
+
+ /**
+ * 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;
+}
+
+/**
+ * Schema definition for the IndexedDB
+ * wallet database.
+ */
export const WalletStoresV1 = {
+ denomLossEvents: describeStoreV2({
+ recordCodec: passthroughCodec<DenomLossEventRecord>(),
+ storeName: "denomLossEvents",
+ keyPath: "denomLossEventId",
+ versionAdded: 9,
+ indexes: {
+ byCurrency: describeIndex("byCurrency", "currency", {
+ versionAdded: 9,
+ }),
+ byStatus: describeIndex("byStatus", "status", {
+ versionAdded: 10,
+ }),
+ },
+ }),
+ transactions: describeStoreV2({
+ recordCodec: passthroughCodec<TransactionRecord>(),
+ storeName: "transactions",
+ keyPath: "transactionItem.transactionId",
+ versionAdded: 7,
+ indexes: {
+ byCurrency: describeIndex("byCurrency", "currency", {
+ versionAdded: 7,
+ }),
+ byExchange: describeIndex("byExchange", "exchanges", {
+ versionAdded: 7,
+ multiEntry: true,
+ }),
+ },
+ }),
+ 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,
+ }),
+ },
+ ),
coins: describeStore(
- describeContents<CoinRecord>("coins", {
+ "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,
+ },
+ ),
},
),
- config: describeStore(
- describeContents<ConfigRecord>("config", { keyPath: "key" }),
- {},
- ),
- auditorTrust: describeStore(
- describeContents<AuditorTrustRecord>("auditorTrust", {
- keyPath: ["currency", "auditorBaseUrl"],
+ reserves: describeStore(
+ "reserves",
+ describeContents<ReserveRecord>({
+ keyPath: "rowId",
+ autoIncrement: true,
}),
{
- byAuditorPub: describeIndex("byAuditorPub", "auditorPub"),
- byUid: describeIndex("byUid", "uids", {
- multiEntry: true,
- }),
+ byReservePub: describeIndex("byReservePub", "reservePub", {}),
},
),
- exchangeTrust: describeStore(
- describeContents<ExchangeTrustRecord>("exchangeTrust", {
- keyPath: ["currency", "exchangeBaseUrl"],
- }),
- {
- byExchangeMasterPub: describeIndex(
- "byExchangeMasterPub",
- "exchangeMasterPub",
- ),
- },
+ config: describeStore(
+ "config",
+ describeContents<ConfigRecord>({ keyPath: "key" }),
+ {},
),
denominations: describeStore(
- describeContents<DenominationRecord>("denominations", {
+ "denominations",
+ describeContents<DenominationRecord>({
keyPath: ["exchangeBaseUrl", "denomPubHash"],
}),
{
@@ -1632,96 +2543,145 @@ export const WalletStoresV1 = {
},
),
exchanges: describeStore(
- describeContents<ExchangeRecord>("exchanges", {
+ "exchanges",
+ describeContents<ExchangeEntryRecord>({
keyPath: "baseUrl",
}),
{},
),
exchangeDetails: describeStore(
- describeContents<ExchangeDetailsRecord>("exchangeDetails", {
- keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
+ "exchangeDetails",
+ describeContents<ExchangeDetailsRecord>({
+ keyPath: "rowId",
+ autoIncrement: true,
}),
- {},
+ {
+ byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", {
+ versionAdded: 2,
+ }),
+ byPointer: describeIndex(
+ "byDetailsPointer",
+ ["exchangeBaseUrl", "currency", "masterPublicKey"],
+ {
+ unique: true,
+ },
+ ),
+ },
),
- proposals: describeStore(
- describeContents<ProposalRecord>("proposals", { keyPath: "proposalId" }),
+ exchangeSignKeys: describeStore(
+ "exchangeSignKeys",
+ describeContents<ExchangeSignkeysRecord>({
+ keyPath: ["exchangeDetailsRowId", "signkeyPub"],
+ }),
{
- byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
- "merchantBaseUrl",
- "orderId",
+ byExchangeDetailsRowId: describeIndex("byExchangeDetailsRowId", [
+ "exchangeDetailsRowId",
]),
},
),
refreshGroups: describeStore(
- describeContents<RefreshGroupRecord>("refreshGroups", {
+ "refreshGroups",
+ describeContents<RefreshGroupRecord>({
keyPath: "refreshGroupId",
}),
+ {
+ byStatus: describeIndex("byStatus", "operationStatus"),
+ byOriginatingTransactionId: describeIndex(
+ "byOriginatingTransactionId",
+ "originatingTransactionId",
+ {
+ versionAdded: 5,
+ },
+ ),
+ },
+ ),
+ refreshSessions: describeStore(
+ "refreshSessions",
+ describeContents<RefreshSessionRecord>({
+ keyPath: ["refreshGroupId", "coinIndex"],
+ }),
{},
),
recoupGroups: describeStore(
- describeContents<RecoupGroupRecord>("recoupGroups", {
+ "recoupGroups",
+ describeContents<RecoupGroupRecord>({
keyPath: "recoupGroupId",
}),
- {},
- ),
- reserves: describeStore(
- describeContents<ReserveRecord>("reserves", { keyPath: "reservePub" }),
{
- byInitialWithdrawalGroupId: describeIndex(
- "byInitialWithdrawalGroupId",
- "initialWithdrawalGroupId",
- ),
+ byStatus: describeIndex("byStatus", "operationStatus", {
+ versionAdded: 6,
+ }),
},
),
purchases: describeStore(
- describeContents<PurchaseRecord>("purchases", { keyPath: "proposalId" }),
+ "purchases",
+ describeContents<PurchaseRecord>({ keyPath: "proposalId" }),
{
+ byStatus: describeIndex("byStatus", "purchaseStatus"),
byFulfillmentUrl: describeIndex(
"byFulfillmentUrl",
- "download.contractData.fulfillmentUrl",
+ "download.fulfillmentUrl",
),
- byMerchantUrlAndOrderId: describeIndex("byMerchantUrlAndOrderId", [
- "download.contractData.merchantBaseUrl",
- "download.contractData.orderId",
+ byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
+ "merchantBaseUrl",
+ "orderId",
]),
},
),
- tips: describeStore(
- describeContents<TipRecord>("tips", { keyPath: "walletTipId" }),
+ rewards: describeStore(
+ "rewards",
+ describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
{
- byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [
- "merchantTipId",
+ byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
+ "merchantRewardId",
"merchantBaseUrl",
]),
+ byStatus: describeIndex("byStatus", "status", {
+ versionAdded: 8,
+ }),
},
),
withdrawalGroups: describeStore(
- describeContents<WithdrawalGroupRecord>("withdrawalGroups", {
+ "withdrawalGroups",
+ describeContents<WithdrawalGroupRecord>({
keyPath: "withdrawalGroupId",
}),
{
- byReservePub: describeIndex("byReservePub", "reservePub"),
+ byStatus: describeIndex("byStatus", "status"),
+ byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", {
+ versionAdded: 2,
+ }),
+ byTalerWithdrawUri: describeIndex(
+ "byTalerWithdrawUri",
+ "wgInfo.bankInfo.talerWithdrawUri",
+ ),
},
),
planchets: describeStore(
- describeContents<PlanchetRecord>("planchets", { keyPath: "coinPub" }),
+ "planchets",
+ describeContents<PlanchetRecord>({ keyPath: "coinPub" }),
{
- byGroupAndIndex: describeIndex("byGroupAndIndex", [
- "withdrawalGroupId",
- "coinIdx",
- ]),
+ byGroupAndIndex: describeIndex(
+ "byGroupAndIndex",
+ ["withdrawalGroupId", "coinIdx"],
+ {
+ unique: true,
+ },
+ ),
byGroup: describeIndex("byGroup", "withdrawalGroupId"),
byCoinEvHash: describeIndex("byCoinEv", "coinEvHash"),
},
),
bankWithdrawUris: describeStore(
- describeContents<BankWithdrawUriRecord>("bankWithdrawUris", {
+ "bankWithdrawUris",
+ describeContents<BankWithdrawUriRecord>({
keyPath: "talerWithdrawUri",
}),
{},
),
backupProviders: describeStore(
- describeContents<BackupProviderRecord>("backupProviders", {
+ "backupProviders",
+ describeContents<BackupProviderRecord>({
keyPath: "baseUrl",
}),
{
@@ -1735,23 +2695,184 @@ export const WalletStoresV1 = {
},
),
depositGroups: describeStore(
- describeContents<DepositGroupRecord>("depositGroups", {
+ "depositGroups",
+ describeContents<DepositGroupRecord>({
keyPath: "depositGroupId",
}),
- {},
+ {
+ byStatus: describeIndex("byStatus", "operationStatus"),
+ },
),
tombstones: describeStore(
- describeContents<TombstoneRecord>("tombstones", { keyPath: "id" }),
+ "tombstones",
+ describeContents<TombstoneRecord>({ keyPath: "id" }),
{},
),
- ghostDepositGroups: describeStore(
- describeContents<GhostDepositGroupRecord>("ghostDepositGroups", {
- keyPath: "contractTermsHash",
+ operationRetries: describeStore(
+ "operationRetries",
+ describeContents<OperationRetryRecord>({
+ keyPath: "id",
+ }),
+ {},
+ ),
+ peerPushCredit: describeStore(
+ "peerPushCredit",
+ describeContents<PeerPushPaymentIncomingRecord>({
+ 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"),
+ },
+ ),
+ bankAccounts: describeStore(
+ "bankAccounts",
+ describeContents<BankAccountsRecord>({
+ keyPath: "uri",
+ }),
+ {},
+ ),
+ contractTerms: describeStore(
+ "contractTerms",
+ describeContents<ContractTermsRecord>({
+ keyPath: "h",
+ }),
+ {},
+ ),
+ userAttention: describeStore(
+ "userAttention",
+ describeContents<UserAttentionRecord>({
+ keyPath: ["entityId", "info.type"],
+ }),
+ {},
+ ),
+ 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",
}),
{},
),
};
+export type WalletDbStoresArr = Array<StoreNames<typeof WalletStoresV1>>;
+
+export type WalletDbReadWriteTransaction<StoresArr extends WalletDbStoresArr> =
+ DbReadWriteTransaction<typeof WalletStoresV1, StoresArr>;
+
+export type WalletDbReadOnlyTransaction<StoresArr extends WalletDbStoresArr> =
+ DbReadOnlyTransaction<typeof WalletStoresV1, StoresArr>;
+
+export type WalletDbAllStoresReadOnlyTransaction<> = DbReadOnlyTransaction<
+ typeof WalletStoresV1,
+ WalletDbStoresArr
+>;
+
+export type WalletDbAllStoresReadWriteTransaction<> = DbReadWriteTransaction<
+ typeof WalletStoresV1,
+ WalletDbStoresArr
+>;
+
+/**
+ * An applied migration.
+ */
+export interface FixupRecord {
+ fixupName: string;
+}
+
+/**
+ * User accounts
+ */
+export interface BankAccountsRecord {
+ uri: string;
+ currency: string;
+ kycCompleted: boolean;
+ alias: string;
+}
+
export interface MetaConfigRecord {
key: string;
value: any;
@@ -1759,7 +2880,518 @@ export interface MetaConfigRecord {
export const walletMetadataStore = {
metaConfig: describeStore(
- describeContents<MetaConfigRecord>("metaConfig", { keyPath: "key" }),
+ "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;
+ };
+}
+
+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.info("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: DbReadWriteTransaction<
+ typeof WalletStoresV1,
+ Array<StoreNames<typeof WalletStoresV1>>
+ >,
+ ): Promise<void>;
+}
+
+/**
+ * Manual migrations between minor versions of the DB schema.
+ */
+export const walletDbFixups: FixupDescription[] = [];
+
+const logger = new Logger("db.ts");
+
+export async function applyFixups(
+ db: DbAccess<typeof WalletStoresV1>,
+): Promise<void> {
+ logger.trace("applying fixups");
+ await db.runAllStoresReadWriteTx({}, async (tx) => {
+ for (const fixupInstruction of walletDbFixups) {
+ logger.trace(`checking fixup ${fixupInstruction.name}`);
+ const fixupRecord = await tx.fixups.get(fixupInstruction.name);
+ if (fixupRecord) {
+ continue;
+ }
+ logger.info(`applying DB fixup ${fixupInstruction.name}`);
+ await fixupInstruction.fn(tx);
+ await tx.fixups.put({
+ fixupName: fixupInstruction.name,
+ });
+ }
+ });
+}
+
+/**
+ * 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]);
+ }
+ 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(
+ WalletStoresV1,
+ 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.runReadWriteTx({ storeNames: ["metaConfig"] }, 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.runReadWriteTx(
+ { storeNames: ["metaConfig"] },
+ 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,
+ );
+
+ const mainDbAccess = new DbAccessImpl(
+ mainDbHandle,
+ WalletStoresV1,
+ {},
+ CancellationToken.CONTINUE,
+ );
+ await applyFixups(mainDbAccess);
+
+ 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();
+ });
+}