taler-typescript-core

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

commit 715d71e22b31ab481bd460edec67b54bf8a23001
parent 68b90ab875e8c68619b778209b2562f58a90c150
Author: Florian Dold <dold@taler.net>
Date:   Sat, 18 Jul 2026 20:01:41 +0200

db: move IdbWalletTransaction to dbtx-indexeddb.ts

Diffstat:
Apackages/taler-wallet-core/src/dbtx-indexeddb.ts | 727+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/dbtx.ts | 680++-----------------------------------------------------------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 3++-
3 files changed, 746 insertions(+), 664 deletions(-)

diff --git a/packages/taler-wallet-core/src/dbtx-indexeddb.ts b/packages/taler-wallet-core/src/dbtx-indexeddb.ts @@ -0,0 +1,727 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * IndexedDB implementation of the backend-neutral {@link WalletDbTransaction} + * data access layer. + * + * All IndexedDB specifics (key ranges, store/index names, the + * <Name>Record storage types) belong here. The interface itself lives in + * dbtx.ts and must stay free of them. + */ + +import { + ContactEntry, + CurrencySpecification, + MailboxConfiguration, + MailboxMessageRecord, + ScopeInfo, + stringifyScopeInfo, + assertUnreachable, + ScopeType, + WalletNotification, +} from "@gnu-taler/taler-util"; +import { GlobalIDB } from "@gnu-taler/idb-bridge"; +import { + ConfigRecord, + WalletPeerPullCredit, + WalletPeerPushDebit, + WalletPeerPushCredit, + WalletPeerPullDebit, + WalletToken, + WalletDenomination, + DenominationVerificationStatus, + OPERATION_STATUS_NONFINAL_FIRST, + OPERATION_STATUS_NONFINAL_LAST, +} from "./db-common.js"; +import type { + ExchangeEntryRecord, + CoinAvailabilityRecord, + RefreshGroupRecord, + WithdrawalGroupRecord, + CoinRecord, + DepositGroupRecord, + ExchangeDetailsRecord, + DonationSummaryRecord, +} from "./db-indexeddb.js"; +import { + PeerPullCreditRecord, + PeerPushDebitRecord, + PeerPushCreditRecord, + PeerPullPaymentIncomingRecord, + PurchaseRecord, + WalletIndexedDbTransaction, +} from "./db-indexeddb.js"; +import type { + GetCurrencyInfoDbResult, + StoreCurrencyInfoDbRequest, + WalletDbTransaction, +} from "./dbtx.js"; + + +function getActiveKeyRange() { + return GlobalIDB.KeyRange.bound( + OPERATION_STATUS_NONFINAL_FIRST, + OPERATION_STATUS_NONFINAL_LAST, + ); +} + +export class IdbWalletTransaction implements WalletDbTransaction { + tx: WalletIndexedDbTransaction; + constructor(tx: WalletIndexedDbTransaction) { + this.tx = tx; + } + + notify(notif: WalletNotification): void { + this.tx.notify(notif); + } + async getCurrencyInfo( + scopeInfo: ScopeInfo, + ): Promise<GetCurrencyInfoDbResult | undefined> { + const tx = this.tx; + const s = stringifyScopeInfo(scopeInfo); + const res = await tx.currencyInfo.get(s); + if (!res) { + return undefined; + } + return { + currencySpec: res.currencySpec, + source: res.source, + }; + } + + async getConfig<T extends ConfigRecord["key"]>( + key: T, + ): Promise<Extract<ConfigRecord, { key: T }> | undefined> { + const tx = this.tx; + return (await tx.config.get(key)) as any; + } + + async upsertConfig(record: ConfigRecord): Promise<void> { + const tx = this.tx; + await tx.config.put(record); + } + + async upsertCurrencyInfo(req: StoreCurrencyInfoDbRequest): Promise<void> { + const tx = this.tx; + await tx.currencyInfo.put({ + scopeInfoStr: stringifyScopeInfo(req.scopeInfo), + currencySpec: req.currencySpec, + source: req.source, + }); + } + + async insertCurrencyInfoUnlessExists( + req: StoreCurrencyInfoDbRequest, + ): Promise<void> { + const tx = this.tx; + const scopeInfoStr = stringifyScopeInfo(req.scopeInfo); + const oldRec = await tx.currencyInfo.get(scopeInfoStr); + if (oldRec) { + return; + } + await tx.currencyInfo.put({ + scopeInfoStr: stringifyScopeInfo(req.scopeInfo), + currencySpec: req.currencySpec, + source: req.source, + }); + } + + async addContact(contact: ContactEntry): Promise<void> { + const tx = this.tx; + await tx.contacts.put({ + alias: contact.alias, + aliasType: contact.aliasType, + mailboxBaseUri: contact.mailboxBaseUri, + mailboxAddress: contact.mailboxAddress, + source: contact.source, + petname: contact.petname, + }); + } + + async deleteContact(alias: string, aliasType: string): Promise<void> { + const tx = this.tx; + await tx.contacts.delete([alias, aliasType]); + } + + async listContacts(): Promise<ContactEntry[]> { + const tx = this.tx; + const records = await tx.contacts.iter().toArray(); + return records.map((r) => ({ + alias: r.alias, + aliasType: r.aliasType, + mailboxBaseUri: r.mailboxBaseUri, + mailboxAddress: r.mailboxAddress, + source: r.source, + petname: r.petname, + })); + } + + async upsertMailboxMessage(message: MailboxMessageRecord): Promise<void> { + const tx = this.tx; + await tx.mailboxMessages.put(message); + } + + async deleteMailboxMessage( + originMailboxBaseUrl: string, + talerUri: string, + ): Promise<void> { + const tx = this.tx; + await tx.mailboxMessages.delete([originMailboxBaseUrl, talerUri]); + } + + async listMailboxMessages(): Promise<MailboxMessageRecord[]> { + const tx = this.tx; + return await tx.mailboxMessages.getAll(); + } + + async getMailboxConfiguration( + mailboxBaseUrl: string, + ): Promise<MailboxConfiguration | undefined> { + const tx = this.tx; + return await tx.mailboxConfigurations.get(mailboxBaseUrl); + } + + async upsertMailboxConfiguration( + mailboxConf: MailboxConfiguration, + ): Promise<void> { + const tx = this.tx; + await tx.mailboxConfigurations.put(mailboxConf); + } + + async getPurchase(proposalId: string): Promise<PurchaseRecord | undefined> { + const tx = this.tx; + return await tx.purchases.get(proposalId); + } + + async listTokens(): Promise<WalletToken[]> { + const tx = this.tx; + const records = await tx.tokens.getAll(); + return records.map((r) => ({ + slug: r.slug, + name: r.name, + description: r.description, + descriptionI18n: r.descriptionI18n, + extraData: r.extraData, + tokenIssuePub: r.tokenIssuePub, + purchaseId: r.purchaseId, + transactionId: r.transactionId, + choiceIndex: r.choiceIndex, + outputIndex: r.outputIndex, + repeatIndex: r.repeatIndex, + merchantBaseUrl: r.merchantBaseUrl, + kind: r.kind, + tokenIssuePubHash: r.tokenIssuePubHash, + tokenFamilyHash: r.tokenFamilyHash, + validAfter: r.validAfter, + validBefore: r.validBefore, + tokenIssueSig: r.tokenIssueSig, + tokenUsePub: r.tokenUsePub, + tokenUsePriv: r.tokenUsePriv, + tokenUseSig: r.tokenUseSig, + })); + } + + async deleteToken(tokenUsePub: string): Promise<void> { + const tx = this.tx; + await tx.tokens.delete(tokenUsePub); + } + + async getTokensByIssuePubHash( + tokenIssuePubHash: string, + ): Promise<WalletToken[]> { + const tx = this.tx; + const records = + await tx.tokens.indexes.byTokenIssuePubHash.getAll(tokenIssuePubHash); + return records.map((r) => ({ + slug: r.slug, + name: r.name, + description: r.description, + descriptionI18n: r.descriptionI18n, + extraData: r.extraData, + tokenIssuePub: r.tokenIssuePub, + purchaseId: r.purchaseId, + transactionId: r.transactionId, + choiceIndex: r.choiceIndex, + outputIndex: r.outputIndex, + repeatIndex: r.repeatIndex, + merchantBaseUrl: r.merchantBaseUrl, + kind: r.kind, + tokenIssuePubHash: r.tokenIssuePubHash, + tokenFamilyHash: r.tokenFamilyHash, + validAfter: r.validAfter, + validBefore: r.validBefore, + tokenIssueSig: r.tokenIssueSig, + tokenUsePub: r.tokenUsePub, + tokenUsePriv: r.tokenUsePriv, + tokenUseSig: r.tokenUseSig, + })); + } + + async getPeerPullCredit( + pursePub: string, + ): Promise<WalletPeerPullCredit | undefined> { + const tx = this.tx; + const r = await tx.peerPullCredit.get(pursePub); + if (!r) { + return undefined; + } + return { + exchangeBaseUrl: r.exchangeBaseUrl, + amount: r.amount, + estimatedAmountEffective: r.estimatedAmountEffective, + pursePub: r.pursePub, + pursePriv: r.pursePriv, + contractTermsHash: r.contractTermsHash, + mergePub: r.mergePub, + mergePriv: r.mergePriv, + contractPub: r.contractPub, + contractPriv: r.contractPriv, + contractEncNonce: r.contractEncNonce, + mergeTimestamp: r.mergeTimestamp, + mergeReserveRowId: r.mergeReserveRowId, + status: r.status, + kycPaytoHash: r.kycPaytoHash, + kycAccessToken: r.kycAccessToken, + kycLastCheckStatus: r.kycLastCheckStatus, + kycLastCheckCode: r.kycLastCheckCode, + kycLastRuleGen: r.kycLastRuleGen, + kycLastAmlReview: r.kycLastAmlReview, + kycLastDeny: r.kycLastDeny, + abortReason: r.abortReason, + failReason: r.failReason, + withdrawalGroupId: r.withdrawalGroupId, + }; + } + + async upsertPeerPullCredit(rec: WalletPeerPullCredit): Promise<void> { + const tx = this.tx; + await tx.peerPullCredit.put({ + exchangeBaseUrl: rec.exchangeBaseUrl, + amount: rec.amount, + estimatedAmountEffective: rec.estimatedAmountEffective, + pursePub: rec.pursePub, + pursePriv: rec.pursePriv, + contractTermsHash: rec.contractTermsHash, + mergePub: rec.mergePub, + mergePriv: rec.mergePriv, + contractPub: rec.contractPub, + contractPriv: rec.contractPriv, + contractEncNonce: rec.contractEncNonce, + mergeTimestamp: rec.mergeTimestamp, + mergeReserveRowId: rec.mergeReserveRowId, + status: rec.status, + kycPaytoHash: rec.kycPaytoHash, + kycAccessToken: rec.kycAccessToken, + kycLastCheckStatus: rec.kycLastCheckStatus, + kycLastCheckCode: rec.kycLastCheckCode, + kycLastRuleGen: rec.kycLastRuleGen, + kycLastAmlReview: rec.kycLastAmlReview, + kycLastDeny: rec.kycLastDeny, + abortReason: rec.abortReason, + failReason: rec.failReason, + withdrawalGroupId: rec.withdrawalGroupId, + }); + } + + async deletePeerPullCredit(pursePub: string): Promise<void> { + const tx = this.tx; + await tx.peerPullCredit.delete(pursePub); + } + + async getPeerPushDebit( + pursePub: string, + ): Promise<WalletPeerPushDebit | undefined> { + const tx = this.tx; + const r = await tx.peerPushDebit.get(pursePub); + if (!r) { + return undefined; + } + return { + exchangeBaseUrl: r.exchangeBaseUrl, + restrictScope: r.restrictScope, + amount: r.amount, + totalCost: r.totalCost, + coinSel: r.coinSel, + contractTermsHash: r.contractTermsHash, + pursePub: r.pursePub, + pursePriv: r.pursePriv, + mergePub: r.mergePub, + mergePriv: r.mergePriv, + contractPriv: r.contractPriv, + contractPub: r.contractPub, + contractEncNonce: r.contractEncNonce, + purseExpiration: r.purseExpiration, + timestampCreated: r.timestampCreated, + abortRefreshGroupId: r.abortRefreshGroupId, + abortReason: r.abortReason, + failReason: r.failReason, + status: r.status, + }; + } + + async upsertPeerPushDebit(rec: WalletPeerPushDebit): Promise<void> { + const tx = this.tx; + await tx.peerPushDebit.put({ + exchangeBaseUrl: rec.exchangeBaseUrl, + restrictScope: rec.restrictScope, + amount: rec.amount, + totalCost: rec.totalCost, + coinSel: rec.coinSel, + contractTermsHash: rec.contractTermsHash, + pursePub: rec.pursePub, + pursePriv: rec.pursePriv, + mergePub: rec.mergePub, + mergePriv: rec.mergePriv, + contractPriv: rec.contractPriv, + contractPub: rec.contractPub, + contractEncNonce: rec.contractEncNonce, + purseExpiration: rec.purseExpiration, + timestampCreated: rec.timestampCreated, + abortRefreshGroupId: rec.abortRefreshGroupId, + abortReason: rec.abortReason, + failReason: rec.failReason, + status: rec.status, + }); + } + + async deletePeerPushDebit(pursePub: string): Promise<void> { + const tx = this.tx; + await tx.peerPushDebit.delete(pursePub); + } + + async getPeerPushCredit( + peerPushCreditId: string, + ): Promise<WalletPeerPushCredit | undefined> { + const tx = this.tx; + const r = await tx.peerPushCredit.get(peerPushCreditId); + if (!r) { + return undefined; + } + return { + peerPushCreditId: r.peerPushCreditId, + exchangeBaseUrl: r.exchangeBaseUrl, + pursePub: r.pursePub, + mergePriv: r.mergePriv, + contractPriv: r.contractPriv, + timestamp: r.timestamp, + estimatedAmountEffective: r.estimatedAmountEffective, + contractTermsHash: r.contractTermsHash, + status: r.status, + abortReason: r.abortReason, + failReason: r.failReason, + withdrawalGroupId: r.withdrawalGroupId, + currency: r.currency, + kycPaytoHash: r.kycPaytoHash, + kycAccessToken: r.kycAccessToken, + kycLastCheckStatus: r.kycLastCheckStatus, + kycLastCheckCode: r.kycLastCheckCode, + kycLastRuleGen: r.kycLastRuleGen, + kycLastAmlReview: r.kycLastAmlReview, + kycLastDeny: r.kycLastDeny, + }; + } + + async upsertPeerPushCredit(rec: WalletPeerPushCredit): Promise<void> { + const tx = this.tx; + await tx.peerPushCredit.put({ + peerPushCreditId: rec.peerPushCreditId, + exchangeBaseUrl: rec.exchangeBaseUrl, + pursePub: rec.pursePub, + mergePriv: rec.mergePriv, + contractPriv: rec.contractPriv, + timestamp: rec.timestamp, + estimatedAmountEffective: rec.estimatedAmountEffective, + contractTermsHash: rec.contractTermsHash, + status: rec.status, + abortReason: rec.abortReason, + failReason: rec.failReason, + withdrawalGroupId: rec.withdrawalGroupId, + currency: rec.currency, + kycPaytoHash: rec.kycPaytoHash, + kycAccessToken: rec.kycAccessToken, + kycLastCheckStatus: rec.kycLastCheckStatus, + kycLastCheckCode: rec.kycLastCheckCode, + kycLastRuleGen: rec.kycLastRuleGen, + kycLastAmlReview: rec.kycLastAmlReview, + kycLastDeny: rec.kycLastDeny, + }); + } + + async deletePeerPushCredit(peerPushCreditId: string): Promise<void> { + const tx = this.tx; + await tx.peerPushCredit.delete(peerPushCreditId); + } + + async getPeerPushCreditByExchangeAndContractPriv( + exchangeBaseUrl: string, + contractPriv: string, + ): Promise<WalletPeerPushCredit | undefined> { + const tx = this.tx; + const r = await tx.peerPushCredit.indexes.byExchangeAndContractPriv.get([ + exchangeBaseUrl, + contractPriv, + ]); + if (!r) { + return undefined; + } + return this.getPeerPushCredit(r.peerPushCreditId); + } + + async getPeerPullDebit( + peerPullDebitId: string, + ): Promise<WalletPeerPullDebit | undefined> { + const tx = this.tx; + const r = await tx.peerPullDebit.get(peerPullDebitId); + if (!r) { + return undefined; + } + return { + peerPullDebitId: r.peerPullDebitId, + pursePub: r.pursePub, + exchangeBaseUrl: r.exchangeBaseUrl, + amount: r.amount, + contractTermsHash: r.contractTermsHash, + timestampCreated: r.timestampCreated, + contractPriv: r.contractPriv, + status: r.status, + totalCostEstimated: r.totalCostEstimated, + abortRefreshGroupId: r.abortRefreshGroupId, + abortReason: r.abortReason, + failReason: r.failReason, + coinSel: r.coinSel, + }; + } + + async upsertPeerPullDebit(rec: WalletPeerPullDebit): Promise<void> { + const tx = this.tx; + await tx.peerPullDebit.put({ + peerPullDebitId: rec.peerPullDebitId, + pursePub: rec.pursePub, + exchangeBaseUrl: rec.exchangeBaseUrl, + amount: rec.amount, + contractTermsHash: rec.contractTermsHash, + timestampCreated: rec.timestampCreated, + contractPriv: rec.contractPriv, + status: rec.status, + totalCostEstimated: rec.totalCostEstimated, + abortRefreshGroupId: rec.abortRefreshGroupId, + abortReason: rec.abortReason, + failReason: rec.failReason, + coinSel: rec.coinSel, + }); + } + + async deletePeerPullDebit(peerPullDebitId: string): Promise<void> { + const tx = this.tx; + await tx.peerPullDebit.delete(peerPullDebitId); + } + + async getPeerPullDebitByExchangeAndContractPriv( + exchangeBaseUrl: string, + contractPriv: string, + ): Promise<WalletPeerPullDebit | undefined> { + const tx = this.tx; + const r = await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([ + exchangeBaseUrl, + contractPriv, + ]); + if (!r) { + return undefined; + } + return this.getPeerPullDebit(r.peerPullDebitId); + } + + async upsertDenomination(rec: WalletDenomination): Promise<void> { + const tx = this.tx; + await tx.denominations.put(rec); + } + + async getDenomination( + exchangeBaseUrl: string, + denomPubHash: string, + ): Promise<WalletDenomination | undefined> { + const tx = this.tx; + return await tx.denominations.get([exchangeBaseUrl, denomPubHash]); + } + + async getDenominationsByVerificationStatus( + verificationStatus: DenominationVerificationStatus, + ): Promise<WalletDenomination[]> { + const tx = this.tx; + return await tx.denominations.indexes.byVerificationStatus.getAll( + verificationStatus, + ); + } + + async getDonationSummaries(): Promise<DonationSummaryRecord[]> { + return await this.tx.donationSummaries.iter().toArray(); + } + + async getExchanges(): Promise<ExchangeEntryRecord[]> { + return await this.tx.exchanges.iter().toArray(); + } + + async getCoinAvailabilities(): Promise<CoinAvailabilityRecord[]> { + return await this.tx.coinAvailability.iter().toArray(); + } + + async getActiveRefreshGroups(): Promise<RefreshGroupRecord[]> { + return await this.tx.refreshGroups.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActiveWithdrawalGroups(): Promise<WithdrawalGroupRecord[]> { + return await this.tx.withdrawalGroups.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPushDebits(): Promise<PeerPushDebitRecord[]> { + return await this.tx.peerPushDebit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPushCredits(): Promise<PeerPushCreditRecord[]> { + return await this.tx.peerPushCredit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPullCredits(): Promise<PeerPullCreditRecord[]> { + return await this.tx.peerPullCredit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPullDebits(): Promise<PeerPullPaymentIncomingRecord[]> { + return await this.tx.peerPullDebit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePurchases(): Promise<PurchaseRecord[]> { + return await this.tx.purchases.indexes.byStatus.getAll(getActiveKeyRange()); + } + + async getCoinsByPubs(coinPubs: string[]): Promise<CoinRecord[]> { + const coins: CoinRecord[] = []; + for (const pub of coinPubs) { + const coin = await this.tx.coins.get(pub); + if (coin) { + coins.push(coin); + } + } + return coins; + } + + async getActiveDepositGroups(): Promise<DepositGroupRecord[]> { + return await this.tx.depositGroups.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getExchangeDetails( + exchangeBaseUrl: string, + ): Promise<ExchangeDetailsRecord | undefined> { + const r = await this.tx.exchanges.get(exchangeBaseUrl); + if (!r || !r.detailsPointer) { + return undefined; + } + return await this.tx.exchangeDetails.indexes.byPointer.get([ + r.baseUrl, + r.detailsPointer.currency, + r.detailsPointer.masterPublicKey, + ]); + } + + async checkExchangeInScope( + exchangeBaseUrl: string, + scope: ScopeInfo, + ): Promise<boolean> { + switch (scope.type) { + case ScopeType.Exchange: { + return scope.url === exchangeBaseUrl; + } + case ScopeType.Global: { + const exchangeDetails = await this.getExchangeDetails(exchangeBaseUrl); + if (!exchangeDetails) { + return false; + } + const gr = + await this.tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get( + [ + exchangeDetails.currency, + exchangeBaseUrl, + exchangeDetails.masterPublicKey, + ], + ); + return gr != null; + } + case ScopeType.Auditor: + throw Error("auditor scope not supported yet"); + default: + assertUnreachable(scope); + } + } + + async getExchangeScopeInfo( + exchangeBaseUrl: string, + currency: string, + ): Promise<ScopeInfo> { + const det = await this.getExchangeDetails(exchangeBaseUrl); + if (!det) { + return { + type: ScopeType.Exchange, + currency: currency, + url: exchangeBaseUrl, + }; + } + const globalExchangeRec = + await this.tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get([ + det.currency, + det.exchangeBaseUrl, + det.masterPublicKey, + ]); + if (globalExchangeRec) { + return { + currency: det.currency, + type: ScopeType.Global, + }; + } else { + for (const aud of det.auditors) { + const globalAuditorRec = + await this.tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get( + [det.currency, aud.auditor_url, aud.auditor_pub], + ); + if (globalAuditorRec) { + return { + currency: det.currency, + type: ScopeType.Auditor, + url: aud.auditor_url, + }; + } + } + } + return { + currency: det.currency, + type: ScopeType.Exchange, + url: det.exchangeBaseUrl, + }; + } +} diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -14,18 +14,31 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +/** + * Backend-neutral data access layer for the wallet database. + * + * This file must only contain the {@link WalletDbTransaction} interface and the + * request/result types it uses. It must not depend on any storage backend: + * the IndexedDB implementation lives in dbtx-indexeddb.ts, and a sqlite3 + * implementation will be added alongside it. + * + * Types crossing this interface belong in db-common.ts and are named + * Wallet<Name>. The <Name>Record types in db-indexeddb.ts describe the + * IndexedDB object stores and must not appear here. + * + * FIXME: The remaining imports from db-indexeddb.js below are a violation of + * that rule, and are removed in phase 0c of the DAL migration (see + * DB_MIGRATION_PLAN.md) by introducing Wallet<Name> counterparts for them. + */ + import { ContactEntry, CurrencySpecification, MailboxConfiguration, MailboxMessageRecord, ScopeInfo, - stringifyScopeInfo, - assertUnreachable, - ScopeType, WalletNotification, } from "@gnu-taler/taler-util"; -import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { ConfigRecord, WalletPeerPullCredit, @@ -35,8 +48,6 @@ import { WalletToken, WalletDenomination, DenominationVerificationStatus, - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, } from "./db-common.js"; import type { ExchangeEntryRecord, @@ -47,14 +58,11 @@ import type { DepositGroupRecord, ExchangeDetailsRecord, DonationSummaryRecord, -} from "./db-indexeddb.js"; -import { PeerPullCreditRecord, PeerPushDebitRecord, PeerPushCreditRecord, PeerPullPaymentIncomingRecord, PurchaseRecord, - WalletIndexedDbTransaction, } from "./db-indexeddb.js"; export interface GetCurrencyInfoDbResult { @@ -268,657 +276,3 @@ export interface WalletDbTransaction { notify(notif: WalletNotification): void; } - -function getActiveKeyRange() { - return GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); -} - -export class IdbWalletTransaction implements WalletDbTransaction { - tx: WalletIndexedDbTransaction; - constructor(tx: WalletIndexedDbTransaction) { - this.tx = tx; - } - - notify(notif: WalletNotification): void { - this.tx.notify(notif); - } - async getCurrencyInfo( - scopeInfo: ScopeInfo, - ): Promise<GetCurrencyInfoDbResult | undefined> { - const tx = this.tx; - const s = stringifyScopeInfo(scopeInfo); - const res = await tx.currencyInfo.get(s); - if (!res) { - return undefined; - } - return { - currencySpec: res.currencySpec, - source: res.source, - }; - } - - async getConfig<T extends ConfigRecord["key"]>( - key: T, - ): Promise<Extract<ConfigRecord, { key: T }> | undefined> { - const tx = this.tx; - return (await tx.config.get(key)) as any; - } - - async upsertConfig(record: ConfigRecord): Promise<void> { - const tx = this.tx; - await tx.config.put(record); - } - - async upsertCurrencyInfo(req: StoreCurrencyInfoDbRequest): Promise<void> { - const tx = this.tx; - await tx.currencyInfo.put({ - scopeInfoStr: stringifyScopeInfo(req.scopeInfo), - currencySpec: req.currencySpec, - source: req.source, - }); - } - - async insertCurrencyInfoUnlessExists( - req: StoreCurrencyInfoDbRequest, - ): Promise<void> { - const tx = this.tx; - const scopeInfoStr = stringifyScopeInfo(req.scopeInfo); - const oldRec = await tx.currencyInfo.get(scopeInfoStr); - if (oldRec) { - return; - } - await tx.currencyInfo.put({ - scopeInfoStr: stringifyScopeInfo(req.scopeInfo), - currencySpec: req.currencySpec, - source: req.source, - }); - } - - async addContact(contact: ContactEntry): Promise<void> { - const tx = this.tx; - await tx.contacts.put({ - alias: contact.alias, - aliasType: contact.aliasType, - mailboxBaseUri: contact.mailboxBaseUri, - mailboxAddress: contact.mailboxAddress, - source: contact.source, - petname: contact.petname, - }); - } - - async deleteContact(alias: string, aliasType: string): Promise<void> { - const tx = this.tx; - await tx.contacts.delete([alias, aliasType]); - } - - async listContacts(): Promise<ContactEntry[]> { - const tx = this.tx; - const records = await tx.contacts.iter().toArray(); - return records.map((r) => ({ - alias: r.alias, - aliasType: r.aliasType, - mailboxBaseUri: r.mailboxBaseUri, - mailboxAddress: r.mailboxAddress, - source: r.source, - petname: r.petname, - })); - } - - async upsertMailboxMessage(message: MailboxMessageRecord): Promise<void> { - const tx = this.tx; - await tx.mailboxMessages.put(message); - } - - async deleteMailboxMessage( - originMailboxBaseUrl: string, - talerUri: string, - ): Promise<void> { - const tx = this.tx; - await tx.mailboxMessages.delete([originMailboxBaseUrl, talerUri]); - } - - async listMailboxMessages(): Promise<MailboxMessageRecord[]> { - const tx = this.tx; - return await tx.mailboxMessages.getAll(); - } - - async getMailboxConfiguration( - mailboxBaseUrl: string, - ): Promise<MailboxConfiguration | undefined> { - const tx = this.tx; - return await tx.mailboxConfigurations.get(mailboxBaseUrl); - } - - async upsertMailboxConfiguration( - mailboxConf: MailboxConfiguration, - ): Promise<void> { - const tx = this.tx; - await tx.mailboxConfigurations.put(mailboxConf); - } - - async getPurchase(proposalId: string): Promise<PurchaseRecord | undefined> { - const tx = this.tx; - return await tx.purchases.get(proposalId); - } - - async listTokens(): Promise<WalletToken[]> { - const tx = this.tx; - const records = await tx.tokens.getAll(); - return records.map((r) => ({ - slug: r.slug, - name: r.name, - description: r.description, - descriptionI18n: r.descriptionI18n, - extraData: r.extraData, - tokenIssuePub: r.tokenIssuePub, - purchaseId: r.purchaseId, - transactionId: r.transactionId, - choiceIndex: r.choiceIndex, - outputIndex: r.outputIndex, - repeatIndex: r.repeatIndex, - merchantBaseUrl: r.merchantBaseUrl, - kind: r.kind, - tokenIssuePubHash: r.tokenIssuePubHash, - tokenFamilyHash: r.tokenFamilyHash, - validAfter: r.validAfter, - validBefore: r.validBefore, - tokenIssueSig: r.tokenIssueSig, - tokenUsePub: r.tokenUsePub, - tokenUsePriv: r.tokenUsePriv, - tokenUseSig: r.tokenUseSig, - })); - } - - async deleteToken(tokenUsePub: string): Promise<void> { - const tx = this.tx; - await tx.tokens.delete(tokenUsePub); - } - - async getTokensByIssuePubHash( - tokenIssuePubHash: string, - ): Promise<WalletToken[]> { - const tx = this.tx; - const records = - await tx.tokens.indexes.byTokenIssuePubHash.getAll(tokenIssuePubHash); - return records.map((r) => ({ - slug: r.slug, - name: r.name, - description: r.description, - descriptionI18n: r.descriptionI18n, - extraData: r.extraData, - tokenIssuePub: r.tokenIssuePub, - purchaseId: r.purchaseId, - transactionId: r.transactionId, - choiceIndex: r.choiceIndex, - outputIndex: r.outputIndex, - repeatIndex: r.repeatIndex, - merchantBaseUrl: r.merchantBaseUrl, - kind: r.kind, - tokenIssuePubHash: r.tokenIssuePubHash, - tokenFamilyHash: r.tokenFamilyHash, - validAfter: r.validAfter, - validBefore: r.validBefore, - tokenIssueSig: r.tokenIssueSig, - tokenUsePub: r.tokenUsePub, - tokenUsePriv: r.tokenUsePriv, - tokenUseSig: r.tokenUseSig, - })); - } - - async getPeerPullCredit( - pursePub: string, - ): Promise<WalletPeerPullCredit | undefined> { - const tx = this.tx; - const r = await tx.peerPullCredit.get(pursePub); - if (!r) { - return undefined; - } - return { - exchangeBaseUrl: r.exchangeBaseUrl, - amount: r.amount, - estimatedAmountEffective: r.estimatedAmountEffective, - pursePub: r.pursePub, - pursePriv: r.pursePriv, - contractTermsHash: r.contractTermsHash, - mergePub: r.mergePub, - mergePriv: r.mergePriv, - contractPub: r.contractPub, - contractPriv: r.contractPriv, - contractEncNonce: r.contractEncNonce, - mergeTimestamp: r.mergeTimestamp, - mergeReserveRowId: r.mergeReserveRowId, - status: r.status, - kycPaytoHash: r.kycPaytoHash, - kycAccessToken: r.kycAccessToken, - kycLastCheckStatus: r.kycLastCheckStatus, - kycLastCheckCode: r.kycLastCheckCode, - kycLastRuleGen: r.kycLastRuleGen, - kycLastAmlReview: r.kycLastAmlReview, - kycLastDeny: r.kycLastDeny, - abortReason: r.abortReason, - failReason: r.failReason, - withdrawalGroupId: r.withdrawalGroupId, - }; - } - - async upsertPeerPullCredit(rec: WalletPeerPullCredit): Promise<void> { - const tx = this.tx; - await tx.peerPullCredit.put({ - exchangeBaseUrl: rec.exchangeBaseUrl, - amount: rec.amount, - estimatedAmountEffective: rec.estimatedAmountEffective, - pursePub: rec.pursePub, - pursePriv: rec.pursePriv, - contractTermsHash: rec.contractTermsHash, - mergePub: rec.mergePub, - mergePriv: rec.mergePriv, - contractPub: rec.contractPub, - contractPriv: rec.contractPriv, - contractEncNonce: rec.contractEncNonce, - mergeTimestamp: rec.mergeTimestamp, - mergeReserveRowId: rec.mergeReserveRowId, - status: rec.status, - kycPaytoHash: rec.kycPaytoHash, - kycAccessToken: rec.kycAccessToken, - kycLastCheckStatus: rec.kycLastCheckStatus, - kycLastCheckCode: rec.kycLastCheckCode, - kycLastRuleGen: rec.kycLastRuleGen, - kycLastAmlReview: rec.kycLastAmlReview, - kycLastDeny: rec.kycLastDeny, - abortReason: rec.abortReason, - failReason: rec.failReason, - withdrawalGroupId: rec.withdrawalGroupId, - }); - } - - async deletePeerPullCredit(pursePub: string): Promise<void> { - const tx = this.tx; - await tx.peerPullCredit.delete(pursePub); - } - - async getPeerPushDebit( - pursePub: string, - ): Promise<WalletPeerPushDebit | undefined> { - const tx = this.tx; - const r = await tx.peerPushDebit.get(pursePub); - if (!r) { - return undefined; - } - return { - exchangeBaseUrl: r.exchangeBaseUrl, - restrictScope: r.restrictScope, - amount: r.amount, - totalCost: r.totalCost, - coinSel: r.coinSel, - contractTermsHash: r.contractTermsHash, - pursePub: r.pursePub, - pursePriv: r.pursePriv, - mergePub: r.mergePub, - mergePriv: r.mergePriv, - contractPriv: r.contractPriv, - contractPub: r.contractPub, - contractEncNonce: r.contractEncNonce, - purseExpiration: r.purseExpiration, - timestampCreated: r.timestampCreated, - abortRefreshGroupId: r.abortRefreshGroupId, - abortReason: r.abortReason, - failReason: r.failReason, - status: r.status, - }; - } - - async upsertPeerPushDebit(rec: WalletPeerPushDebit): Promise<void> { - const tx = this.tx; - await tx.peerPushDebit.put({ - exchangeBaseUrl: rec.exchangeBaseUrl, - restrictScope: rec.restrictScope, - amount: rec.amount, - totalCost: rec.totalCost, - coinSel: rec.coinSel, - contractTermsHash: rec.contractTermsHash, - pursePub: rec.pursePub, - pursePriv: rec.pursePriv, - mergePub: rec.mergePub, - mergePriv: rec.mergePriv, - contractPriv: rec.contractPriv, - contractPub: rec.contractPub, - contractEncNonce: rec.contractEncNonce, - purseExpiration: rec.purseExpiration, - timestampCreated: rec.timestampCreated, - abortRefreshGroupId: rec.abortRefreshGroupId, - abortReason: rec.abortReason, - failReason: rec.failReason, - status: rec.status, - }); - } - - async deletePeerPushDebit(pursePub: string): Promise<void> { - const tx = this.tx; - await tx.peerPushDebit.delete(pursePub); - } - - async getPeerPushCredit( - peerPushCreditId: string, - ): Promise<WalletPeerPushCredit | undefined> { - const tx = this.tx; - const r = await tx.peerPushCredit.get(peerPushCreditId); - if (!r) { - return undefined; - } - return { - peerPushCreditId: r.peerPushCreditId, - exchangeBaseUrl: r.exchangeBaseUrl, - pursePub: r.pursePub, - mergePriv: r.mergePriv, - contractPriv: r.contractPriv, - timestamp: r.timestamp, - estimatedAmountEffective: r.estimatedAmountEffective, - contractTermsHash: r.contractTermsHash, - status: r.status, - abortReason: r.abortReason, - failReason: r.failReason, - withdrawalGroupId: r.withdrawalGroupId, - currency: r.currency, - kycPaytoHash: r.kycPaytoHash, - kycAccessToken: r.kycAccessToken, - kycLastCheckStatus: r.kycLastCheckStatus, - kycLastCheckCode: r.kycLastCheckCode, - kycLastRuleGen: r.kycLastRuleGen, - kycLastAmlReview: r.kycLastAmlReview, - kycLastDeny: r.kycLastDeny, - }; - } - - async upsertPeerPushCredit(rec: WalletPeerPushCredit): Promise<void> { - const tx = this.tx; - await tx.peerPushCredit.put({ - peerPushCreditId: rec.peerPushCreditId, - exchangeBaseUrl: rec.exchangeBaseUrl, - pursePub: rec.pursePub, - mergePriv: rec.mergePriv, - contractPriv: rec.contractPriv, - timestamp: rec.timestamp, - estimatedAmountEffective: rec.estimatedAmountEffective, - contractTermsHash: rec.contractTermsHash, - status: rec.status, - abortReason: rec.abortReason, - failReason: rec.failReason, - withdrawalGroupId: rec.withdrawalGroupId, - currency: rec.currency, - kycPaytoHash: rec.kycPaytoHash, - kycAccessToken: rec.kycAccessToken, - kycLastCheckStatus: rec.kycLastCheckStatus, - kycLastCheckCode: rec.kycLastCheckCode, - kycLastRuleGen: rec.kycLastRuleGen, - kycLastAmlReview: rec.kycLastAmlReview, - kycLastDeny: rec.kycLastDeny, - }); - } - - async deletePeerPushCredit(peerPushCreditId: string): Promise<void> { - const tx = this.tx; - await tx.peerPushCredit.delete(peerPushCreditId); - } - - async getPeerPushCreditByExchangeAndContractPriv( - exchangeBaseUrl: string, - contractPriv: string, - ): Promise<WalletPeerPushCredit | undefined> { - const tx = this.tx; - const r = await tx.peerPushCredit.indexes.byExchangeAndContractPriv.get([ - exchangeBaseUrl, - contractPriv, - ]); - if (!r) { - return undefined; - } - return this.getPeerPushCredit(r.peerPushCreditId); - } - - async getPeerPullDebit( - peerPullDebitId: string, - ): Promise<WalletPeerPullDebit | undefined> { - const tx = this.tx; - const r = await tx.peerPullDebit.get(peerPullDebitId); - if (!r) { - return undefined; - } - return { - peerPullDebitId: r.peerPullDebitId, - pursePub: r.pursePub, - exchangeBaseUrl: r.exchangeBaseUrl, - amount: r.amount, - contractTermsHash: r.contractTermsHash, - timestampCreated: r.timestampCreated, - contractPriv: r.contractPriv, - status: r.status, - totalCostEstimated: r.totalCostEstimated, - abortRefreshGroupId: r.abortRefreshGroupId, - abortReason: r.abortReason, - failReason: r.failReason, - coinSel: r.coinSel, - }; - } - - async upsertPeerPullDebit(rec: WalletPeerPullDebit): Promise<void> { - const tx = this.tx; - await tx.peerPullDebit.put({ - peerPullDebitId: rec.peerPullDebitId, - pursePub: rec.pursePub, - exchangeBaseUrl: rec.exchangeBaseUrl, - amount: rec.amount, - contractTermsHash: rec.contractTermsHash, - timestampCreated: rec.timestampCreated, - contractPriv: rec.contractPriv, - status: rec.status, - totalCostEstimated: rec.totalCostEstimated, - abortRefreshGroupId: rec.abortRefreshGroupId, - abortReason: rec.abortReason, - failReason: rec.failReason, - coinSel: rec.coinSel, - }); - } - - async deletePeerPullDebit(peerPullDebitId: string): Promise<void> { - const tx = this.tx; - await tx.peerPullDebit.delete(peerPullDebitId); - } - - async getPeerPullDebitByExchangeAndContractPriv( - exchangeBaseUrl: string, - contractPriv: string, - ): Promise<WalletPeerPullDebit | undefined> { - const tx = this.tx; - const r = await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([ - exchangeBaseUrl, - contractPriv, - ]); - if (!r) { - return undefined; - } - return this.getPeerPullDebit(r.peerPullDebitId); - } - - async upsertDenomination(rec: WalletDenomination): Promise<void> { - const tx = this.tx; - await tx.denominations.put(rec); - } - - async getDenomination( - exchangeBaseUrl: string, - denomPubHash: string, - ): Promise<WalletDenomination | undefined> { - const tx = this.tx; - return await tx.denominations.get([exchangeBaseUrl, denomPubHash]); - } - - async getDenominationsByVerificationStatus( - verificationStatus: DenominationVerificationStatus, - ): Promise<WalletDenomination[]> { - const tx = this.tx; - return await tx.denominations.indexes.byVerificationStatus.getAll( - verificationStatus, - ); - } - - async getDonationSummaries(): Promise<DonationSummaryRecord[]> { - return await this.tx.donationSummaries.iter().toArray(); - } - - async getExchanges(): Promise<ExchangeEntryRecord[]> { - return await this.tx.exchanges.iter().toArray(); - } - - async getCoinAvailabilities(): Promise<CoinAvailabilityRecord[]> { - return await this.tx.coinAvailability.iter().toArray(); - } - - async getActiveRefreshGroups(): Promise<RefreshGroupRecord[]> { - return await this.tx.refreshGroups.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getActiveWithdrawalGroups(): Promise<WithdrawalGroupRecord[]> { - return await this.tx.withdrawalGroups.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getActivePeerPushDebits(): Promise<PeerPushDebitRecord[]> { - return await this.tx.peerPushDebit.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getActivePeerPushCredits(): Promise<PeerPushCreditRecord[]> { - return await this.tx.peerPushCredit.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getActivePeerPullCredits(): Promise<PeerPullCreditRecord[]> { - return await this.tx.peerPullCredit.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getActivePeerPullDebits(): Promise<PeerPullPaymentIncomingRecord[]> { - return await this.tx.peerPullDebit.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getActivePurchases(): Promise<PurchaseRecord[]> { - return await this.tx.purchases.indexes.byStatus.getAll(getActiveKeyRange()); - } - - async getCoinsByPubs(coinPubs: string[]): Promise<CoinRecord[]> { - const coins: CoinRecord[] = []; - for (const pub of coinPubs) { - const coin = await this.tx.coins.get(pub); - if (coin) { - coins.push(coin); - } - } - return coins; - } - - async getActiveDepositGroups(): Promise<DepositGroupRecord[]> { - return await this.tx.depositGroups.indexes.byStatus.getAll( - getActiveKeyRange(), - ); - } - - async getExchangeDetails( - exchangeBaseUrl: string, - ): Promise<ExchangeDetailsRecord | undefined> { - const r = await this.tx.exchanges.get(exchangeBaseUrl); - if (!r || !r.detailsPointer) { - return undefined; - } - return await this.tx.exchangeDetails.indexes.byPointer.get([ - r.baseUrl, - r.detailsPointer.currency, - r.detailsPointer.masterPublicKey, - ]); - } - - async checkExchangeInScope( - exchangeBaseUrl: string, - scope: ScopeInfo, - ): Promise<boolean> { - switch (scope.type) { - case ScopeType.Exchange: { - return scope.url === exchangeBaseUrl; - } - case ScopeType.Global: { - const exchangeDetails = await this.getExchangeDetails(exchangeBaseUrl); - if (!exchangeDetails) { - return false; - } - const gr = - await this.tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get( - [ - exchangeDetails.currency, - exchangeBaseUrl, - exchangeDetails.masterPublicKey, - ], - ); - return gr != null; - } - case ScopeType.Auditor: - throw Error("auditor scope not supported yet"); - default: - assertUnreachable(scope); - } - } - - async getExchangeScopeInfo( - exchangeBaseUrl: string, - currency: string, - ): Promise<ScopeInfo> { - const det = await this.getExchangeDetails(exchangeBaseUrl); - if (!det) { - return { - type: ScopeType.Exchange, - currency: currency, - url: exchangeBaseUrl, - }; - } - const globalExchangeRec = - await this.tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get([ - det.currency, - det.exchangeBaseUrl, - det.masterPublicKey, - ]); - if (globalExchangeRec) { - return { - currency: det.currency, - type: ScopeType.Global, - }; - } else { - for (const aud of det.auditors) { - const globalAuditorRec = - await this.tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get( - [det.currency, aud.auditor_url, aud.auditor_pub], - ); - if (globalAuditorRec) { - return { - currency: det.currency, - type: ScopeType.Auditor, - url: aud.auditor_url, - }; - } - } - } - return { - currency: det.currency, - type: ScopeType.Exchange, - url: det.exchangeBaseUrl, - }; - } -} diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -330,7 +330,8 @@ import { openTalerDatabase, walletDbFixups, } from "./db-indexeddb.js"; -import { IdbWalletTransaction, WalletDbTransaction } from "./dbtx.js"; +import { WalletDbTransaction } from "./dbtx.js"; +import { IdbWalletTransaction } from "./dbtx-indexeddb.js"; import { isCandidateWithdrawableDenomRec, isWithdrawableDenom,