taler-typescript-core

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

commit 43889193dcfa8058a647b2c876a63d4c0d5d64cc
parent 59071e0e4a2ba260643e5efe924c266c00972273
Author: Florian Dold <florian@dold.me>
Date:   Wed, 15 Jul 2026 22:48:44 +0200

wallet-core: start with DAL

Diffstat:
Mpackages/taler-wallet-core/src/contacts.ts | 14+++++++-------
Mpackages/taler-wallet-core/src/db-common.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db-indexeddb.ts | 54++++--------------------------------------------------
Mpackages/taler-wallet-core/src/dbtx.ts | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/mailbox.ts | 22+++++++++++++---------
Mpackages/taler-wallet-core/src/preset-exchanges.ts | 6++++--
Mpackages/taler-wallet-core/src/testing.ts | 2+-
7 files changed, 177 insertions(+), 70 deletions(-)

diff --git a/packages/taler-wallet-core/src/contacts.ts b/packages/taler-wallet-core/src/contacts.ts @@ -30,15 +30,15 @@ import { Logger, NotificationType, } from "@gnu-taler/taler-util"; -import { ContactRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; +import { WalletDbTransaction } from "./dbtx.js"; import { WalletExecutionContext } from "./wallet.js"; const logger = new Logger("contacts.ts"); async function makeContactListItem( wex: WalletExecutionContext, - tx: WalletIndexedDbTransaction, - r: ContactRecord, + tx: WalletDbTransaction, + r: ContactEntry, //lastError: TalerErrorDetail | undefined, ): Promise<ContactEntry> { const listItem: ContactEntry = { @@ -60,7 +60,7 @@ export async function addContact( req: AddContactRequest, ): Promise<EmptyObject> { await wex.runLegacyWalletDbTx(async (tx) => { - tx.contacts.put(req.contact); + await tx.wtx.addContact(req.contact); tx.notify({ type: NotificationType.ContactAdded, contact: req.contact, @@ -77,7 +77,7 @@ export async function deleteContact( req: DeleteContactRequest, ): Promise<EmptyObject> { await wex.runLegacyWalletDbTx(async (tx) => { - await tx.contacts.delete([req.contact.alias, req.contact.aliasType]); + await tx.wtx.deleteContact(req.contact.alias, req.contact.aliasType); tx.notify({ type: NotificationType.ContactDeleted, contact: req.contact, @@ -95,9 +95,9 @@ export async function listContacts( ): Promise<ContactListResponse> { const contacts: ContactEntry[] = []; await wex.runLegacyWalletDbTx(async (tx) => { - const contactsRecords = await tx.contacts.iter().toArray(); + const contactsRecords = await tx.wtx.listContacts(); for (const contactRec of contactsRecords) { - const li = await makeContactListItem(wex, tx, contactRec); + const li = await makeContactListItem(wex, tx.wtx, contactRec); contacts.push(li); } }); diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts @@ -454,6 +454,52 @@ export enum ConfigRecordKey { DonauConfig = "donauConfig", } +export interface DonauConfig { + donauBaseUrl: string; + donauTaxId: string; + /** Tax ID hash, salted with donauSalt */ + donauTaxIdHash: string; + /** 32 byte salt, base32crockford encoded */ + donauSalt: string; +} + +export interface WalletBackupConfState { + deviceId: string; + walletRootPub: string; + walletRootPriv: string; + + /** + * Last hash of the canonicalized plain-text backup. + */ + lastBackupPlainHash?: string; + + /** + * Timestamp stored in the last backup. + */ + lastBackupTimestamp?: DbPreciseTimestamp; + + /** + * Last time we tried to do a backup. + */ + lastBackupCheckTimestamp?: DbPreciseTimestamp; + lastBackupNonce?: string; +} + +/** + * Configuration key/value entries to configure + * the wallet. + */ +export type ConfigRecord = + | { + key: ConfigRecordKey.WalletBackupState; + value: WalletBackupConfState; + } + | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean | number } + | { key: ConfigRecordKey.TestLoopTx; value: number } + | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp } + | { key: ConfigRecordKey.MaterializedTransactionsVersion; value: number } + | { key: ConfigRecordKey.DonauConfig; value: DonauConfig }; + export enum RecoupOperationStatus { Pending = 0x0100_0000, Suspended = 0x0110_0000, diff --git a/packages/taler-wallet-core/src/db-indexeddb.ts b/packages/taler-wallet-core/src/db-indexeddb.ts @@ -84,7 +84,7 @@ import { } from "@gnu-taler/taler-util"; import { DbRetryInfo, TaskIdentifiers } from "./common.js"; import { - ConfigRecordKey, + ConfigRecord, DbPreciseTimestamp, DbProtocolTimestamp, DenomLossStatus, @@ -92,6 +92,7 @@ import { DepositElementStatus, DepositOperationStatus, DonationReceiptStatus, + DonauConfig, ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, PeerPullDebitRecordStatus, @@ -106,6 +107,7 @@ import { RefundGroupStatus, RefundItemStatus, ReserveRecordStatus, + WalletBackupConfState, WithdrawalGroupStatus, timestampProtocolFromDb, } from "./db-common.js"; @@ -123,6 +125,7 @@ import { describeStoreV2, openDatabase, } from "./query.js"; +export { ConfigRecord, DonauConfig, WalletBackupConfState }; /** * This file contains the database schema of the Taler wallet together @@ -1315,52 +1318,6 @@ export interface PurchaseRecord { refundAmountAwaiting: AmountString | undefined; } -export interface DonauConfig { - donauBaseUrl: string; - donauTaxId: string; - /** Tax ID hash, salted with donauSalt */ - donauTaxIdHash: string; - /** 32 byte salt, base32crockford encoded */ - donauSalt: string; -} - -/** - * Configuration key/value entries to configure - * the wallet. - */ -export type ConfigRecord = - | { - key: ConfigRecordKey.WalletBackupState; - value: WalletBackupConfState; - } - | { key: ConfigRecordKey.CurrencyDefaultsApplied; value: boolean | number } - | { key: ConfigRecordKey.TestLoopTx; value: number } - | { key: ConfigRecordKey.LastInitInfo; value: DbProtocolTimestamp } - | { key: ConfigRecordKey.MaterializedTransactionsVersion; value: number } - | { key: ConfigRecordKey.DonauConfig; value: DonauConfig }; - -export interface WalletBackupConfState { - deviceId: string; - walletRootPub: string; - walletRootPriv: string; - - /** - * Last hash of the canonicalized plain-text backup. - */ - lastBackupPlainHash?: string; - - /** - * Timestamp stored in the last backup. - */ - lastBackupTimestamp?: DbPreciseTimestamp; - - /** - * Last time we tried to do a backup. - */ - lastBackupCheckTimestamp?: DbPreciseTimestamp; - lastBackupNonce?: string; -} - // FIXME: Should these be numeric codes? export const enum WithdrawalRecordType { BankManual = "bank-manual", @@ -2292,9 +2249,6 @@ export interface DonationSummaryRecord { amountReceiptsSubmitted: AmountString; } -/** - * Record for contacts - */ export interface ContactRecord { /** * The mailbox URI of this contact diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -15,11 +15,15 @@ */ import { + ContactEntry, CurrencySpecification, + MailboxConfiguration, + MailboxMessageRecord, ScopeInfo, stringifyScopeInfo, } from "@gnu-taler/taler-util"; -import { ConfigRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; +import { ConfigRecord } from "./db-common.js"; +import { PurchaseRecord, WalletIndexedDbTransaction } from "./db-indexeddb.js"; export interface GetCurrencyInfoDbResult { /** @@ -48,6 +52,8 @@ export interface WalletDbTransaction { key: T, ): Promise<Extract<ConfigRecord, { key: T }> | undefined>; + upsertConfig(record: ConfigRecord): Promise<void>; + /** * Store currency info for a scope. * @@ -58,6 +64,29 @@ export interface WalletDbTransaction { insertCurrencyInfoUnlessExists( req: StoreCurrencyInfoDbRequest, ): Promise<void>; + + addContact(contact: ContactEntry): Promise<void>; + + deleteContact(alias: string, aliasType: string): Promise<void>; + + listContacts(): Promise<ContactEntry[]>; + + upsertMailboxMessage(message: MailboxMessageRecord): Promise<void>; + + deleteMailboxMessage( + originMailboxBaseUrl: string, + talerUri: string, + ): Promise<void>; + + listMailboxMessages(): Promise<MailboxMessageRecord[]>; + + getMailboxConfiguration( + mailboxBaseUrl: string, + ): Promise<MailboxConfiguration | undefined>; + + upsertMailboxConfiguration(mailboxConf: MailboxConfiguration): Promise<void>; + + getPurchase(proposalId: string): Promise<PurchaseRecord | undefined>; } export class IdbWalletTransaction implements WalletDbTransaction { @@ -87,6 +116,11 @@ export class IdbWalletTransaction implements WalletDbTransaction { 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({ @@ -111,4 +145,71 @@ export class IdbWalletTransaction implements WalletDbTransaction { 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); + } } diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts @@ -68,7 +68,7 @@ export async function addMailboxMessage( req: AddMailboxMessageRequest, ): Promise<EmptyObject> { await wex.runLegacyWalletDbTx(async (tx) => { - tx.mailboxMessages.put(req.message); + await tx.wtx.upsertMailboxMessage(req.message); tx.notify({ type: NotificationType.MailboxMessageAdded, message: req.message, @@ -85,10 +85,10 @@ export async function deleteMailboxMessage( req: DeleteMailboxMessageRequest, ): Promise<EmptyObject> { await wex.runLegacyWalletDbTx(async (tx) => { - tx.mailboxMessages.delete([ + await tx.wtx.deleteMailboxMessage( req.message.originMailboxBaseUrl, req.message.talerUri, - ]); + ); tx.notify({ type: NotificationType.MailboxMessageDeleted, message: req.message, @@ -105,7 +105,7 @@ export async function listMailboxMessages( req: EmptyObject, ): Promise<MailboxMessagesResponse> { const messages = await wex.runLegacyWalletDbTx(async (tx) => { - return await tx.mailboxMessages.getAll(); + return await tx.wtx.listMailboxMessages(); }); return { messages: messages }; } @@ -181,8 +181,10 @@ export async function getMailbox( ): Promise<GetMailboxResponse> { return await wex.runLegacyWalletDbTx(async (tx) => { return { - mailboxConfiguration: await tx.mailboxConfigurations.get(mailboxBaseUrl.mailboxBaseUrl), - } + mailboxConfiguration: await tx.wtx.getMailboxConfiguration( + mailboxBaseUrl.mailboxBaseUrl, + ), + }; }); } @@ -198,7 +200,7 @@ export async function createNewMailbox( const hpkeKey = encodeCrock(hpkeCreateSecretKey()); const privKey = encodeCrock(keys.eddsaPriv); const hAddress = encodeCrock(sha512(keys.eddsaPub)); - const nowInAYear = AbsoluteTime.addDuration( + const nowInAYear = AbsoluteTime.addDuration( AbsoluteTime.now(), Duration.fromSpec({ years: 1, @@ -225,7 +227,7 @@ export async function createNewMailbox( ); } await wex.runLegacyWalletDbTx(async (tx) => { - return await tx.mailboxConfigurations.put(mailboxConf); + return await tx.wtx.upsertMailboxConfiguration(mailboxConf); }); return mailboxConf; } @@ -274,7 +276,9 @@ export async function refreshMailbox( default: throw Error("unable to get mailbox service config"); } - const res = await mailboxClient.getMessages({ hMailbox: mailboxConf.hAddress }); + const res = await mailboxClient.getMessages({ + hMailbox: mailboxConf.hAddress, + }); switch (res.case) { case "ok": const hpkeSk: Uint8Array = decodeCrock(mailboxConf.privateEncryptionKey); diff --git a/packages/taler-wallet-core/src/preset-exchanges.ts b/packages/taler-wallet-core/src/preset-exchanges.ts @@ -73,7 +73,9 @@ const builtinExchanges: BuiltinExchange[] = [ */ export async function fillDefaults(wex: WalletExecutionContext): Promise<void> { await wex.runLegacyWalletDbTx(async (tx) => { - let appliedRec = await tx.config.get("currencyDefaultsApplied"); + let appliedRec = await tx.wtx.getConfig( + ConfigRecordKey.CurrencyDefaultsApplied, + ); let appliedVersion = appliedRec ? !!appliedRec.value : 0; if (appliedRec != null) { if (appliedRec.value === true) { @@ -101,7 +103,7 @@ export async function fillDefaults(wex: WalletExecutionContext): Promise<void> { exch.currencySpec, ); } - await tx.config.put({ + await tx.wtx.upsertConfig({ key: ConfigRecordKey.CurrencyDefaultsApplied, value: currentDefaultsVersion, }); diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts @@ -976,7 +976,7 @@ export async function testPay( const purchase = await wex.runLegacyWalletDbTx(async (tx) => { const parsedTx = parseTransactionIdentifier(r.transactionId); checkLogicInvariant(parsedTx?.tag === TransactionType.Payment); - return tx.purchases.get(parsedTx.proposalId); + return tx.wtx.getPurchase(parsedTx.proposalId); }); checkLogicInvariant(!!purchase); return {