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.ts1737
1 files changed, 1736 insertions, 1 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index bc0e45017..c1076b900 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1,4 +1,3 @@
-import { MetaStores, Stores } from "./types/dbTypes";
import {
openDatabase,
Database,
@@ -11,8 +10,12 @@ import {
IDBDatabase,
IDBObjectStore,
IDBTransaction,
+ IDBKeyPath,
} from "@gnu-taler/idb-bridge";
import { Logger } from "./util/logging";
+import { AmountJson, AmountString, Auditor, CoinDepositPermission, ContractTerms, Duration, ExchangeSignKeyJson, InternationalizedString, MerchantInfo, Product, RefreshReason, ReserveTransaction, TalerErrorDetails, Timestamp } from "@gnu-taler/taler-util";
+import { RetryInfo } from "./util/retries.js";
+import { PayCoinSelection } from "./util/coinSelection.js";
/**
* Name of the Taler database. This is effectively the major
@@ -166,3 +169,1735 @@ export async function openTalerDatabase(
export function deleteTalerDatabase(idbFactory: IDBFactory): void {
Database.deleteDatabase(idbFactory, TALER_DB_NAME);
}
+
+
+export enum ReserveRecordStatus {
+ /**
+ * Reserve must be registered with the bank.
+ */
+ REGISTERING_BANK = "registering-bank",
+
+ /**
+ * 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",
+
+ /**
+ * Querying reserve status with the exchange.
+ */
+ QUERYING_STATUS = "querying-status",
+
+ /**
+ * The corresponding withdraw record has been created.
+ * No further processing is done, unless explicitly requested
+ * by the user.
+ */
+ DORMANT = "dormant",
+
+ /**
+ * The bank aborted the withdrawal.
+ */
+ BANK_ABORTED = "bank-aborted",
+}
+
+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;
+
+ confirmUrl?: string;
+
+ /**
+ * Exchange payto URI that the bank will use to fund the reserve.
+ */
+ exchangePaytoUri: string;
+}
+
+/**
+ * A reserve record as stored in the wallet's database.
+ */
+export interface ReserveRecord {
+ /**
+ * The reserve public key.
+ */
+ reservePub: string;
+
+ /**
+ * The reserve private key.
+ */
+ reservePriv: string;
+
+ /**
+ * The exchange base URL.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Currency of the reserve.
+ */
+ currency: string;
+
+ /**
+ * Time when the reserve was created.
+ */
+ timestampCreated: Timestamp;
+
+ /**
+ * 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.
+ */
+ timestampReserveInfoPosted: Timestamp | undefined;
+
+ /**
+ * Time when the reserve was confirmed by the bank.
+ *
+ * Set to undefined if not confirmed yet.
+ */
+ timestampBankConfirmed: Timestamp | undefined;
+
+ /**
+ * Wire information (as payto URI) for the bank account that
+ * transfered funds for this reserve.
+ */
+ senderWire?: string;
+
+ /**
+ * Amount that was sent by the user to fund the reserve.
+ */
+ instructedAmount: AmountJson;
+
+ /**
+ * Extra state for when this is a withdrawal involving
+ * a Taler-integrated bank.
+ */
+ bankInfo?: ReserveBankInfo;
+
+ initialWithdrawalGroupId: string;
+
+ /**
+ * Did we start the first withdrawal for this reserve?
+ *
+ * We only report a pending withdrawal for the reserve before
+ * the first withdrawal has started.
+ */
+ initialWithdrawalStarted: boolean;
+
+ /**
+ * Initial denomination selection, stored here so that
+ * we can show this information in the transactions/balances
+ * before we have a withdrawal group.
+ */
+ initialDenomSel: DenomSelectionState;
+
+ reserveStatus: ReserveRecordStatus;
+
+ /**
+ * Was a reserve query requested? If so, query again instead
+ * of going into dormant status.
+ */
+ requestedQuery: boolean;
+
+ /**
+ * Time of the last successful status query.
+ */
+ lastSuccessfulStatusQuery: Timestamp | undefined;
+
+ /**
+ * 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.
+ */
+ retryInfo: RetryInfo;
+
+ /**
+ * Last error that happened in a reserve operation
+ * (either talking to the bank or the exchange).
+ */
+ lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * Auditor record as stored with currencies in the exchange database.
+ */
+export interface AuditorRecord {
+ /**
+ * Base url of the auditor.
+ */
+ baseUrl: string;
+
+ /**
+ * Public signing key of the auditor.
+ */
+ auditorPub: string;
+
+ /**
+ * Time when the auditing expires.
+ */
+ expirationStamp: number;
+}
+
+/**
+ * Exchange for currencies as stored in the wallet's currency
+ * information database.
+ */
+export interface ExchangeForCurrencyRecord {
+ /**
+ * FIXME: unused?
+ */
+ exchangeMasterPub: string;
+
+ /**
+ * Base URL of the exchange.
+ */
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Information about a currency as displayed in the wallet's database.
+ */
+export interface CurrencyRecord {
+ /**
+ * Name of the currency.
+ */
+ name: string;
+
+ /**
+ * Number of fractional digits to show when rendering the currency.
+ */
+ fractionalDigits: number;
+
+ /**
+ * Auditors that the wallet trusts for this currency.
+ */
+ auditors: AuditorRecord[];
+
+ /**
+ * Exchanges that the wallet trusts for this currency.
+ */
+ exchanges: ExchangeForCurrencyRecord[];
+}
+
+/**
+ * Status of a denomination.
+ */
+export enum DenominationStatus {
+ /**
+ * Verification was delayed.
+ */
+ Unverified = "unverified",
+ /**
+ * Verified as valid.
+ */
+ VerifiedGood = "verified-good",
+ /**
+ * Verified as invalid.
+ */
+ VerifiedBad = "verified-bad",
+}
+
+/**
+ * Denomination record as stored in the wallet's database.
+ */
+export interface DenominationRecord {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: AmountJson;
+
+ /**
+ * The denomination public key.
+ */
+ denomPub: string;
+
+ /**
+ * Hash of the denomination public key.
+ * Stored in the database for faster lookups.
+ */
+ denomPubHash: string;
+
+ /**
+ * Fee for withdrawing.
+ */
+ feeWithdraw: AmountJson;
+
+ /**
+ * Fee for depositing.
+ */
+ feeDeposit: AmountJson;
+
+ /**
+ * Fee for refreshing.
+ */
+ feeRefresh: AmountJson;
+
+ /**
+ * Fee for refunding.
+ */
+ feeRefund: AmountJson;
+
+ /**
+ * Validity start date of the denomination.
+ */
+ stampStart: Timestamp;
+
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ stampExpireWithdraw: Timestamp;
+
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ stampExpireLegal: Timestamp;
+
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ stampExpireDeposit: Timestamp;
+
+ /**
+ * Signature by the exchange's master key over the denomination
+ * information.
+ */
+ masterSig: string;
+
+ /**
+ * Did we verify the signature on the denomination?
+ *
+ * FIXME: Rename to "verificationStatus"?
+ */
+ status: DenominationStatus;
+
+ /**
+ * Was this denomination still offered by the exchange the last time
+ * we checked?
+ * Only false when the exchange redacts a previously published denomination.
+ */
+ isOffered: boolean;
+
+ /**
+ * Did the exchange revoke the denomination?
+ * When this field is set to true in the database, the same transaction
+ * should also mark all affected coins as revoked.
+ */
+ isRevoked: boolean;
+
+ /**
+ * Base URL of the exchange.
+ */
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Details about the exchange that we only know after
+ * querying /keys and /wire.
+ */
+export interface ExchangeDetails {
+ /**
+ * Master public key of the exchange.
+ */
+ masterPublicKey: string;
+
+ /**
+ * Auditors (partially) auditing the exchange.
+ */
+ auditors: Auditor[];
+
+ /**
+ * Currency that the exchange offers.
+ */
+ currency: string;
+
+ /**
+ * 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.
+ */
+ signingKeys: ExchangeSignKeyJson[];
+
+ /**
+ * Timestamp for last update.
+ */
+ lastUpdateTime: Timestamp;
+
+ /**
+ * When should we next update the information about the exchange?
+ */
+ nextUpdateTime: Timestamp;
+}
+
+export enum ExchangeUpdateStatus {
+ FetchKeys = "fetch-keys",
+ FetchWire = "fetch-wire",
+ FetchTerms = "fetch-terms",
+ FinalizeUpdate = "finalize-update",
+ Finished = "finished",
+}
+
+export interface ExchangeBankAccount {
+ payto_uri: string;
+ master_sig: string;
+}
+
+export interface ExchangeWireInfo {
+ feesForType: { [wireMethod: string]: WireFee[] };
+ accounts: ExchangeBankAccount[];
+}
+
+export enum ExchangeUpdateReason {
+ Initial = "initial",
+ Forced = "forced",
+ Scheduled = "scheduled",
+}
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+export interface ExchangeRecord {
+ /**
+ * Base url of the exchange.
+ */
+ baseUrl: string;
+
+ /**
+ * Did we finish adding the exchange?
+ */
+ addComplete: boolean;
+
+ /**
+ * Is this a permanent or temporary exchange record?
+ */
+ permanent: boolean;
+
+ /**
+ * Was the exchange added as a built-in exchange?
+ */
+ builtIn: boolean;
+
+ /**
+ * Details, once known.
+ */
+ details: ExchangeDetails | undefined;
+
+ /**
+ * Mapping from wire method type to the wire fee.
+ */
+ wireInfo: ExchangeWireInfo | undefined;
+
+ /**
+ * 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;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceLastEtag: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceAcceptedEtag: string | undefined;
+
+ /**
+ * Time when the update to the exchange has been started or
+ * undefined if no update is in progress.
+ */
+ updateStarted: Timestamp | undefined;
+
+ /**
+ * Status of updating the info about the exchange.
+ */
+ updateStatus: ExchangeUpdateStatus;
+
+ updateReason?: ExchangeUpdateReason;
+
+ lastError?: TalerErrorDetails;
+
+ /**
+ * Retry status for fetching updated information about the exchange.
+ */
+ retryInfo: RetryInfo;
+
+ /**
+ * Next time that we should check if coins need to be refreshed.
+ *
+ * Updated whenever the exchange's denominations are updated or when
+ * the refresh check has been done.
+ */
+ nextRefreshCheck?: Timestamp;
+}
+
+/**
+ * A coin that isn't yet signed by an exchange.
+ */
+export interface PlanchetRecord {
+ /**
+ * Public key of the coin.
+ */
+ coinPub: string;
+
+ /**
+ * Private key of the coin.
+ */
+ coinPriv: string;
+
+ /**
+ * Withdrawal group that this planchet belongs to
+ * (or the empty string).
+ */
+ withdrawalGroupId: string;
+
+ /**
+ * Index within the withdrawal group (or -1).
+ */
+ coinIdx: number;
+
+ withdrawalDone: boolean;
+
+ 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;
+
+ denomPubHash: string;
+
+ denomPub: string;
+
+ blindingKey: string;
+
+ withdrawSig: string;
+
+ coinEv: string;
+
+ coinEvHash: string;
+
+ coinValue: AmountJson;
+
+ isFromTip: boolean;
+}
+
+/**
+ * Planchet for a coin during refrehs.
+ */
+export interface RefreshPlanchet {
+ /**
+ * Public key for the coin.
+ */
+ publicKey: string;
+
+ /**
+ * Private key for the coin.
+ */
+ privateKey: string;
+
+ /**
+ * Blinded public key.
+ */
+ coinEv: string;
+
+ coinEvHash: string;
+
+ /**
+ * Blinding key used.
+ */
+ blindingKey: string;
+}
+
+/**
+ * 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",
+}
+
+export enum CoinSourceType {
+ Withdraw = "withdraw",
+ Refresh = "refresh",
+ Tip = "tip",
+}
+
+export interface WithdrawCoinSource {
+ type: CoinSourceType.Withdraw;
+
+ /**
+ * Can be the empty string for orphaned coins.
+ */
+ withdrawalGroupId: string;
+
+ /**
+ * Index of the coin in the withdrawal session.
+ */
+ coinIndex: number;
+
+ /**
+ * Reserve public key for the reserve we got this coin from.
+ */
+ reservePub: string;
+}
+
+export interface RefreshCoinSource {
+ type: CoinSourceType.Refresh;
+ oldCoinPub: string;
+}
+
+export interface TipCoinSource {
+ type: CoinSourceType.Tip;
+ walletTipId: string;
+ coinIndex: number;
+}
+
+export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
+
+/**
+ * CoinRecord as stored in the "coins" data store
+ * of the wallet database.
+ */
+export interface CoinRecord {
+ /**
+ * Where did the coin come from? Used for recouping coins.
+ */
+ coinSource: CoinSource;
+
+ /**
+ * Public key of the coin.
+ */
+ coinPub: string;
+
+ /**
+ * Private key to authorize operations on the coin.
+ */
+ 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;
+
+ /**
+ * Unblinded signature by the exchange.
+ */
+ denomSig: string;
+
+ /**
+ * Amount that's left on the coin.
+ */
+ currentAmount: AmountJson;
+
+ /**
+ * Base URL that identifies the exchange from which we got the
+ * coin.
+ */
+ 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.
+ */
+ blindingKey: string;
+
+ /**
+ * Hash of the coin envelope.
+ *
+ * Stored here for indexing purposes, so that when looking at a
+ * reserve history, we can quickly find the coin for a withdrawal transaction.
+ */
+ coinEvHash: string;
+
+ /**
+ * Status of the coin.
+ */
+ status: CoinStatus;
+}
+
+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.
+ */
+ REPURCHASE = "repurchase",
+}
+
+export interface ProposalDownload {
+ /**
+ * The contract that was offered by the merchant.
+ */
+ contractTermsRaw: any;
+
+ contractData: WalletContractData;
+}
+
+/**
+ * Record for a downloaded order, stored in the wallet's database.
+ */
+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;
+
+ /**
+ * Retry info, even present when the operation isn't active to allow indexing
+ * on the next retry timestamp.
+ */
+ retryInfo: RetryInfo;
+
+ lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * Status of a tip we got from a merchant.
+ */
+export interface TipRecord {
+ lastError: TalerErrorDetails | undefined;
+
+ /**
+ * Has the user accepted the tip? Only after the tip has been accepted coins
+ * withdrawn from the tip may be used.
+ */
+ acceptedTimestamp: Timestamp | undefined;
+
+ /**
+ * The tipped amount.
+ */
+ tipAmountRaw: AmountJson;
+
+ tipAmountEffective: AmountJson;
+
+ /**
+ * Timestamp, the tip can't be picked up anymore after this deadline.
+ */
+ tipExpiration: Timestamp;
+
+ /**
+ * The exchange that will sign our coins, chosen by the merchant.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Base URL of the merchant that is giving us the tip.
+ */
+ merchantBaseUrl: string;
+
+ /**
+ * Denomination selection made by the wallet for picking up
+ * this tip.
+ */
+ denomsSel: DenomSelectionState;
+
+ /**
+ * Tip ID chosen by the wallet.
+ */
+ walletTipId: string;
+
+ /**
+ * Secret seed used to derive planchets for this tip.
+ */
+ secretSeed: string;
+
+ /**
+ * The merchant's identifier for this tip.
+ */
+ merchantTipId: string;
+
+ createdTimestamp: Timestamp;
+
+ /**
+ * Timestamp for when the wallet finished picking up the tip
+ * from the merchant.
+ */
+ pickedUpTimestamp: Timestamp | undefined;
+
+ /**
+ * Retry info, even present when the operation isn't active to allow indexing
+ * on the next retry timestamp.
+ */
+ retryInfo: RetryInfo;
+}
+
+export interface RefreshGroupRecord {
+ /**
+ * Retry info, even present when the operation isn't active to allow indexing
+ * on the next retry timestamp.
+ */
+ retryInfo: RetryInfo;
+
+ lastError: TalerErrorDetails | undefined;
+
+ lastErrorPerCoin: { [coinIndex: number]: TalerErrorDetails };
+
+ refreshGroupId: string;
+
+ reason: RefreshReason;
+
+ oldCoinPubs: string[];
+
+ refreshSessionPerCoin: (RefreshSessionRecord | undefined)[];
+
+ inputPerCoin: AmountJson[];
+
+ estimatedOutputPerCoin: AmountJson[];
+
+ /**
+ * Flag for each coin whether refreshing finished.
+ * If a coin can't be refreshed (remaining value too small),
+ * it will be marked as finished, but no refresh session will
+ * be created.
+ */
+ finishedPerCoin: boolean[];
+
+ timestampCreated: Timestamp;
+
+ /**
+ * Timestamp when the refresh session finished.
+ */
+ timestampFinished: Timestamp | undefined;
+}
+
+/**
+ * Ongoing refresh
+ */
+export interface RefreshSessionRecord {
+ /**
+ * 512-bit secret that can be used to derive
+ * the other cryptographic material for the refresh session.
+ *
+ * FIXME: We currently store the derived material, but
+ * should always derive it.
+ */
+ sessionSecretSeed: string;
+
+ /**
+ * Sum of the value of denominations we want
+ * to withdraw in this session, without fees.
+ */
+ amountRefreshOutput: AmountJson;
+
+ /**
+ * Hashed denominations of the newly requested coins.
+ */
+ newDenoms: {
+ denomPubHash: string;
+ count: number;
+ }[];
+
+ /**
+ * The no-reveal-index after we've done the melting.
+ */
+ norevealIndex?: number;
+}
+
+/**
+ * Wire fee for one wire method as stored in the
+ * wallet's database.
+ */
+export interface WireFee {
+ /**
+ * Fee for wire transfers.
+ */
+ wireFee: AmountJson;
+
+ /**
+ * Fees to close and refund a reserve.
+ */
+ closingFee: AmountJson;
+
+ /**
+ * Start date of the fee.
+ */
+ startStamp: Timestamp;
+
+ /**
+ * End date of the fee.
+ */
+ endStamp: Timestamp;
+
+ /**
+ * Signature made by the exchange master key.
+ */
+ sig: string;
+}
+
+/**
+ * Record to store information about a refund event.
+ *
+ * All information about a refund is stored with the purchase,
+ * this event is just for the history.
+ *
+ * The event is only present for completed refunds.
+ */
+export interface RefundEventRecord {
+ timestamp: Timestamp;
+ merchantExecutionTimestamp: Timestamp;
+ refundGroupId: string;
+ proposalId: string;
+}
+
+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;
+
+export interface WalletRefundItemCommon {
+ // Execution time as claimed by the merchant
+ executionTime: Timestamp;
+
+ /**
+ * Time when the wallet became aware of the refund.
+ */
+ obtainedTime: Timestamp;
+
+ refundAmount: AmountJson;
+
+ refundFee: AmountJson;
+
+ /**
+ * 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.
+ */
+ totalRefreshCostBound: AmountJson;
+
+ coinPub: string;
+
+ rtransactionId: number;
+}
+
+/**
+ * Failed refund, either because the merchant did
+ * something wrong or it expired.
+ */
+export interface WalletRefundFailedItem extends WalletRefundItemCommon {
+ type: RefundState.Failed;
+}
+
+export interface WalletRefundPendingItem extends WalletRefundItemCommon {
+ type: RefundState.Pending;
+}
+
+export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
+ type: RefundState.Applied;
+}
+
+export enum RefundReason {
+ /**
+ * Normal refund given by the merchant.
+ */
+ NormalRefund = "normal-refund",
+ /**
+ * Refund from an aborted payment.
+ */
+ AbortRefund = "abort-pay-refund",
+}
+
+/**
+ * Record stored for every time we successfully submitted
+ * a payment to the merchant (both first time and re-play).
+ */
+export interface PayEventRecord {
+ proposalId: string;
+ sessionId: string | undefined;
+ isReplay: boolean;
+ timestamp: Timestamp;
+}
+
+export interface ExchangeUpdatedEventRecord {
+ exchangeBaseUrl: string;
+ timestamp: Timestamp;
+}
+
+export interface ReserveUpdatedEventRecord {
+ amountReserveBalance: string;
+ amountExpected: string;
+ reservePub: string;
+ timestamp: Timestamp;
+ reserveUpdateId: string;
+ newHistoryTransactions: ReserveTransaction[];
+}
+
+export interface AllowedAuditorInfo {
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export interface AllowedExchangeInfo {
+ exchangeBaseUrl: string;
+ exchangePub: string;
+}
+
+/**
+ * Data extracted from the contract terms that is relevant for payment
+ * processing in the wallet.
+ */
+export interface WalletContractData {
+ products?: Product[];
+ summaryI18n: { [lang_tag: string]: string } | undefined;
+
+ /**
+ * 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.
+ */
+ 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",
+}
+
+/**
+ * Record that stores status information about one purchase, starting from when
+ * the customer accepts a proposal. Includes refund status if applicable.
+ */
+export interface PurchaseRecord {
+ /**
+ * Proposal ID for this purchase. Uniquely identifies the
+ * purchase and the proposal.
+ */
+ proposalId: string;
+
+ /**
+ * Private key for the nonce.
+ */
+ noncePriv: string;
+
+ /**
+ * Public key for the nonce.
+ */
+ noncePub: string;
+
+ /**
+ * Downloaded and parsed proposal data.
+ */
+ download: ProposalDownload;
+
+ /**
+ * Deposit permissions, available once the user has accepted the payment.
+ *
+ * This value is cached and derived from payCoinSelection.
+ */
+ coinDepositPermissions: CoinDepositPermission[] | undefined;
+
+ payCoinSelection: PayCoinSelection;
+
+ /**
+ * Pending removals from pay coin selection.
+ *
+ * Used when a the pay coin selection needs to be changed
+ * because a coin became known as double-spent or invalid,
+ * but a new coin selection can't immediately be done, as
+ * there is not enough balance (e.g. when waiting for a refresh).
+ */
+ pendingRemovedCoinPubs?: string[];
+
+ totalPayCost: AmountJson;
+
+ /**
+ * Timestamp of the first time that sending a payment to the merchant
+ * for this purchase was successful.
+ */
+ timestampFirstSuccessfulPay: Timestamp | undefined;
+
+ merchantPaySig: string | undefined;
+
+ /**
+ * When was the purchase made?
+ * Refers to the time that the user accepted.
+ */
+ timestampAccept: Timestamp;
+
+ /**
+ * 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.
+ */
+ timestampLastRefundStatus: Timestamp | undefined;
+
+ /**
+ * Last session signature that we submitted to /pay (if any).
+ */
+ lastSessionId: string | undefined;
+
+ /**
+ * Set for the first payment, or on re-plays.
+ */
+ paymentSubmitPending: boolean;
+
+ /**
+ * 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.
+ */
+ refundStatusRetryInfo: RetryInfo;
+
+ /**
+ * Last error (or undefined) for querying the refund status with the merchant.
+ */
+ lastRefundStatusError: TalerErrorDetails | undefined;
+
+ /**
+ * Continue querying the refund status until this deadline has expired.
+ */
+ autoRefundDeadline: Timestamp | undefined;
+}
+
+/**
+ * Configuration key/value entries to configure
+ * the wallet.
+ */
+export interface ConfigRecord<T> {
+ key: string;
+ value: T;
+}
+
+/**
+ * FIXME: Eliminate this in favor of DenomSelectionState.
+ */
+export interface DenominationSelectionInfo {
+ totalCoinValue: AmountJson;
+ totalWithdrawCost: AmountJson;
+ selectedDenoms: {
+ /**
+ * How many times do we withdraw this denomination?
+ */
+ count: number;
+ denom: DenominationRecord;
+ }[];
+}
+
+/**
+ * Selected denominations withn some extra info.
+ */
+export interface DenomSelectionState {
+ totalCoinValue: AmountJson;
+ totalWithdrawCost: AmountJson;
+ selectedDenoms: {
+ denomPubHash: string;
+ count: number;
+ }[];
+}
+
+/**
+ * Group of withdrawal operations that need to be executed.
+ * (Either for a normal withdrawal or from a tip.)
+ *
+ * The withdrawal group record is only created after we know
+ * the coin selection we want to withdraw.
+ */
+export interface WithdrawalGroupRecord {
+ withdrawalGroupId: string;
+
+ /**
+ * Secret seed used to derive planchets.
+ */
+ secretSeed: string;
+
+ reservePub: string;
+
+ exchangeBaseUrl: string;
+
+ /**
+ * When was the withdrawal operation started started?
+ * Timestamp in milliseconds.
+ */
+ timestampStart: Timestamp;
+
+ /**
+ * When was the withdrawal operation completed?
+ */
+ timestampFinish?: Timestamp;
+
+ /**
+ * Amount including fees (i.e. the amount subtracted from the
+ * reserve to withdraw all coins in this withdrawal session).
+ */
+ rawWithdrawalAmount: AmountJson;
+
+ denomsSel: DenomSelectionState;
+
+ /**
+ * Retry info, always present even on completed operations so that indexing works.
+ */
+ retryInfo: RetryInfo;
+
+ lastError: TalerErrorDetails | undefined;
+}
+
+export interface BankWithdrawUriRecord {
+ /**
+ * The withdraw URI we got from the bank.
+ */
+ talerWithdrawUri: string;
+
+ /**
+ * Reserve that was created for the withdraw URI.
+ */
+ reservePub: string;
+}
+
+/**
+ * Status of recoup operations that were grouped together.
+ *
+ * The remaining amount of involved coins should be set to zero
+ * in the same transaction that inserts the RecoupGroupRecord.
+ */
+export interface RecoupGroupRecord {
+ /**
+ * Unique identifier for the recoup group record.
+ */
+ recoupGroupId: string;
+
+ timestampStarted: Timestamp;
+
+ timestampFinished: Timestamp | undefined;
+
+ /**
+ * Public keys that identify the coins being recouped
+ * as part of this session.
+ *
+ * (Structured like this to enable multiEntry indexing in IndexedDB.)
+ */
+ coinPubs: string[];
+
+ /**
+ * Array of flags to indicate whether the recoup finished on each individual coin.
+ */
+ recoupFinishedPerCoin: boolean[];
+
+ /**
+ * 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 occured, if any.
+ */
+ lastError: TalerErrorDetails | undefined;
+}
+
+export enum ImportPayloadType {
+ CoreSchema = "core-schema",
+}
+
+export enum BackupProviderStatus {
+ PaymentRequired = "payment-required",
+ Ready = "ready",
+}
+
+export interface BackupProviderRecord {
+ baseUrl: string;
+
+ /**
+ * Terms of service of the provider.
+ * Might be unavailable in the DB in certain situations
+ * (such as loading a recovery document).
+ */
+ terms?: {
+ supportedProtocolVersion: string;
+ annualFee: AmountString;
+ storageLimitInMegabytes: number;
+ };
+
+ active: boolean;
+
+ /**
+ * Hash of the last encrypted backup that we already merged
+ * or successfully uploaded ourselves.
+ */
+ lastBackupHash?: string;
+
+ /**
+ * Clock of the last backup that we already
+ * merged.
+ */
+ lastBackupClock?: number;
+
+ lastBackupTimestamp?: Timestamp;
+
+ /**
+ * Proposal that we're currently trying to pay for.
+ *
+ * (Also included in paymentProposalIds.)
+ */
+ currentPaymentProposalId?: string;
+
+ /**
+ * Proposals that were used to pay (or attempt to pay) the provider.
+ *
+ * Stored to display a history of payments to the provider, and
+ * to make sure that the wallet isn't overpaying.
+ */
+ paymentProposalIds: string[];
+
+ /**
+ * Next scheduled backup.
+ */
+ nextBackupTimestamp?: Timestamp;
+
+ /**
+ * Retry info.
+ */
+ retryInfo: RetryInfo;
+
+ /**
+ * Last error that occured, if any.
+ */
+ lastError: TalerErrorDetails | undefined;
+}
+
+/**
+ * Group of deposits made by the wallet.
+ */
+export interface DepositGroupRecord {
+ depositGroupId: string;
+
+ merchantPub: string;
+ merchantPriv: string;
+
+ noncePriv: string;
+ noncePub: string;
+
+ /**
+ * Wire information used by all deposits in this
+ * deposit group.
+ */
+ wire: {
+ payto_uri: string;
+ salt: string;
+ };
+
+ /**
+ * Verbatim contract terms.
+ */
+ contractTermsRaw: ContractTerms;
+
+ contractTermsHash: string;
+
+ payCoinSelection: PayCoinSelection;
+
+ totalPayCost: AmountJson;
+
+ effectiveDepositAmount: AmountJson;
+
+ depositedPerCoin: boolean[];
+
+ timestampCreated: Timestamp;
+
+ timestampFinished: Timestamp | undefined;
+
+ lastError: TalerErrorDetails | undefined;
+
+ /**
+ * Retry info.
+ */
+ retryInfo: RetryInfo;
+}
+
+/**
+ * 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.
+ */
+export interface GhostDepositGroupRecord {
+ /**
+ * When multiple deposits for the same contract terms hash
+ * have a different timestamp, we choose the earliest one.
+ */
+ timestamp: Timestamp;
+
+ contractTermsHash: string;
+
+ deposits: {
+ coinPub: string;
+ amount: AmountString;
+ timestamp: Timestamp;
+ depositFee: AmountString;
+ merchantPub: string;
+ coinSig: string;
+ wireHash: string;
+ }[];
+}
+
+class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
+ constructor() {
+ super("exchanges", { keyPath: "baseUrl" });
+ }
+}
+
+class CoinsStore extends Store<"coins", CoinRecord> {
+ constructor() {
+ super("coins", { keyPath: "coinPub" });
+ }
+
+ exchangeBaseUrlIndex = new Index<
+ "coins",
+ "exchangeBaseUrl",
+ string,
+ CoinRecord
+ >(this, "exchangeBaseUrl", "exchangeBaseUrl");
+
+ denomPubHashIndex = new Index<
+ "coins",
+ "denomPubHashIndex",
+ string,
+ CoinRecord
+ >(this, "denomPubHashIndex", "denomPubHash");
+
+ coinEvHashIndex = new Index<"coins", "coinEvHashIndex", string, CoinRecord>(
+ this,
+ "coinEvHashIndex",
+ "coinEvHash",
+ );
+}
+
+class ProposalsStore extends Store<"proposals", ProposalRecord> {
+ constructor() {
+ super("proposals", { keyPath: "proposalId" });
+ }
+ urlAndOrderIdIndex = new Index<
+ "proposals",
+ "urlIndex",
+ string,
+ ProposalRecord
+ >(this, "urlIndex", ["merchantBaseUrl", "orderId"]);
+}
+
+class PurchasesStore extends Store<"purchases", PurchaseRecord> {
+ constructor() {
+ super("purchases", { keyPath: "proposalId" });
+ }
+
+ fulfillmentUrlIndex = new Index<
+ "purchases",
+ "fulfillmentUrlIndex",
+ string,
+ PurchaseRecord
+ >(this, "fulfillmentUrlIndex", "download.contractData.fulfillmentUrl");
+
+ orderIdIndex = new Index<"purchases", "orderIdIndex", string, PurchaseRecord>(
+ this,
+ "orderIdIndex",
+ ["download.contractData.merchantBaseUrl", "download.contractData.orderId"],
+ );
+}
+
+class DenominationsStore extends Store<"denominations", DenominationRecord> {
+ constructor() {
+ // cast needed because of bug in type annotations
+ super("denominations", {
+ keyPath: (["exchangeBaseUrl", "denomPubHash"] as any) as IDBKeyPath,
+ });
+ }
+ exchangeBaseUrlIndex = new Index<
+ "denominations",
+ "exchangeBaseUrlIndex",
+ string,
+ DenominationRecord
+ >(this, "exchangeBaseUrlIndex", "exchangeBaseUrl");
+}
+
+class CurrenciesStore extends Store<"currencies", CurrencyRecord> {
+ constructor() {
+ super("currencies", { keyPath: "name" });
+ }
+}
+
+class ConfigStore extends Store<"config", ConfigRecord<any>> {
+ constructor() {
+ super("config", { keyPath: "key" });
+ }
+}
+
+class ReservesStore extends Store<"reserves", ReserveRecord> {
+ constructor() {
+ super("reserves", { keyPath: "reservePub" });
+ }
+}
+
+class TipsStore extends Store<"tips", TipRecord> {
+ constructor() {
+ super("tips", { keyPath: "walletTipId" });
+ }
+ // Added in version 2
+ byMerchantTipIdAndBaseUrl = new Index<
+ "tips",
+ "tipsByMerchantTipIdAndOriginIndex",
+ [string, string],
+ TipRecord
+ >(this, "tipsByMerchantTipIdAndOriginIndex", [
+ "merchantTipId",
+ "merchantBaseUrl",
+ ]);
+}
+
+class WithdrawalGroupsStore extends Store<
+ "withdrawals",
+ WithdrawalGroupRecord
+> {
+ constructor() {
+ super("withdrawals", { keyPath: "withdrawalGroupId" });
+ }
+ byReservePub = new Index<
+ "withdrawals",
+ "withdrawalsByReserveIndex",
+ string,
+ WithdrawalGroupRecord
+ >(this, "withdrawalsByReserveIndex", "reservePub");
+}
+
+class PlanchetsStore extends Store<"planchets", PlanchetRecord> {
+ constructor() {
+ super("planchets", { keyPath: "coinPub" });
+ }
+ byGroupAndIndex = new Index<
+ "planchets",
+ "withdrawalGroupAndCoinIdxIndex",
+ string,
+ PlanchetRecord
+ >(this, "withdrawalGroupAndCoinIdxIndex", ["withdrawalGroupId", "coinIdx"]);
+ byGroup = new Index<
+ "planchets",
+ "withdrawalGroupIndex",
+ string,
+ PlanchetRecord
+ >(this, "withdrawalGroupIndex", "withdrawalGroupId");
+
+ coinEvHashIndex = new Index<
+ "planchets",
+ "coinEvHashIndex",
+ string,
+ PlanchetRecord
+ >(this, "coinEvHashIndex", "coinEvHash");
+}
+
+/**
+ * This store is effectively a materialized index for
+ * reserve records that are for a bank-integrated withdrawal.
+ */
+class BankWithdrawUrisStore extends Store<
+ "bankWithdrawUris",
+ BankWithdrawUriRecord
+> {
+ constructor() {
+ super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
+ }
+}
+
+/**
+ */
+class BackupProvidersStore extends Store<
+ "backupProviders",
+ BackupProviderRecord
+> {
+ constructor() {
+ super("backupProviders", { keyPath: "baseUrl" });
+ }
+}
+
+class DepositGroupsStore extends Store<"depositGroups", DepositGroupRecord> {
+ constructor() {
+ super("depositGroups", { keyPath: "depositGroupId" });
+ }
+}
+
+/**
+ * The stores and indices for the wallet database.
+ */
+export const Stores = {
+ coins: new CoinsStore(),
+ config: new ConfigStore(),
+ currencies: new CurrenciesStore(),
+ denominations: new DenominationsStore(),
+ exchanges: new ExchangesStore(),
+ proposals: new ProposalsStore(),
+ refreshGroups: new Store<"refreshGroups", RefreshGroupRecord>(
+ "refreshGroups",
+ {
+ keyPath: "refreshGroupId",
+ },
+ ),
+ recoupGroups: new Store<"recoupGroups", RecoupGroupRecord>("recoupGroups", {
+ keyPath: "recoupGroupId",
+ }),
+ reserves: new ReservesStore(),
+ purchases: new PurchasesStore(),
+ tips: new TipsStore(),
+ withdrawalGroups: new WithdrawalGroupsStore(),
+ planchets: new PlanchetsStore(),
+ bankWithdrawUris: new BankWithdrawUrisStore(),
+ backupProviders: new BackupProvidersStore(),
+ depositGroups: new DepositGroupsStore(),
+ ghostDepositGroups: new Store<"ghostDepositGroups", GhostDepositGroupRecord>(
+ "ghostDepositGroups",
+ {
+ keyPath: "contractTermsHash",
+ },
+ ),
+};
+
+export class MetaConfigStore extends Store<"metaConfig", ConfigRecord<any>> {
+ constructor() {
+ super("metaConfig", { keyPath: "key" });
+ }
+}
+
+export const MetaStores = {
+ metaConfig: new MetaConfigStore(),
+};