taler-typescript-core

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

commit 667f5fec1c36dc23d494e3dc86f335529a6ce272
parent 1faea487783c82362af5e87631c4cf28b73dd27e
Author: Florian Dold <dold@taler.net>
Date:   Thu, 10 Sep 2026 01:31:46 +0200

wallet-core: add durable storage for coin recovery

Store recovery worklists per exchange and authenticated melt indices
per exchange and commitment in dedicated IndexedDB and SQLite stores.
Preserve them through database export, import and backend conversion.

Allow refresh sessions to retain an original CS blinding seed supplied
by another wallet.

Diffstat:
Mpackages/taler-wallet-core/src/db/indexeddb/schema.ts | 18+++++++++++++++++-
Mpackages/taler-wallet-core/src/db/indexeddb/transaction.ts | 42++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db/migration/converter.test.ts | 8++++----
Mpackages/taler-wallet-core/src/db/migration/converter.ts | 10++++++++++
Mpackages/taler-wallet-core/src/db/records.ts | 22++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db/sqlite/schema-migrations.test.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db/sqlite/schema.ts | 37+++++++++++++++++++++++++++++++------
Mpackages/taler-wallet-core/src/db/sqlite/transaction.ts | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mpackages/taler-wallet-core/src/db/testing/conformance-cases.ts | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db/transaction.ts | 31+++++++++++++++++++++++++++++++
10 files changed, 366 insertions(+), 14 deletions(-)

diff --git a/packages/taler-wallet-core/src/db/indexeddb/schema.ts b/packages/taler-wallet-core/src/db/indexeddb/schema.ts @@ -69,6 +69,8 @@ import { WalletRecoupGroup, WalletRefreshGroup, WalletRefreshSession, + WalletCoinRecovery, + WalletRefreshMeltConfirmation, WalletRefundGroup, WalletRefundItem, WalletReserve, @@ -168,7 +170,7 @@ export const TALER_WALLET_DB_GENERATION_PREFIX = `${TALER_WALLET_MAIN_DB_NAME}-g * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 35; +export const WALLET_DB_MINOR_VERSION = 36; export interface BankWithdrawUriRecord { /** @@ -950,6 +952,20 @@ export const WalletIndexedDbStoresV1 = { }), }, ), + coinRecoveries: describeStoreV2({ + recordCodec: passthroughCodec<WalletCoinRecovery>(), + storeName: "coinRecoveries", + keyPath: "exchangeBaseUrl", + versionAdded: 36, + indexes: {}, + }), + refreshMeltConfirmations: describeStoreV2({ + recordCodec: passthroughCodec<WalletRefreshMeltConfirmation>(), + storeName: "refreshMeltConfirmations", + keyPath: ["exchangeBaseUrl", "refreshCommitment"], + versionAdded: 36, + indexes: {}, + }), recoupGroups: describeStore( "recoupGroups", describeContents<WalletRecoupGroup>({ diff --git a/packages/taler-wallet-core/src/db/indexeddb/transaction.ts b/packages/taler-wallet-core/src/db/indexeddb/transaction.ts @@ -64,6 +64,8 @@ import { WalletReserve, WalletRefreshGroup, WalletRefreshSession, + WalletCoinRecovery, + WalletRefreshMeltConfirmation, WalletWithdrawalGroup, WalletPlanchet, WalletDonationSummary, @@ -144,6 +146,8 @@ export class IdbWalletTransaction implements WalletDbTransaction { coins: "coins", planchets: "planchets", refreshSessions: "refreshSessions", + coinRecoveries: "coinRecoveries", + refreshMeltConfirmations: "refreshMeltConfirmations", coinHistory: "coinHistory", coinAvailability: "coinAvailabilityV2", refundGroups: "refundGroups", @@ -1440,6 +1444,44 @@ export class IdbWalletTransaction implements WalletDbTransaction { return await this.tx.refreshSessions.getAll(); } + async getCoinRecovery( + exchangeBaseUrl: string, + ): Promise<WalletCoinRecovery | undefined> { + return await this.tx.coinRecoveries.get(exchangeBaseUrl); + } + + async upsertCoinRecovery(rec: WalletCoinRecovery): Promise<void> { + await this.tx.coinRecoveries.put(rec); + } + + async listAllCoinRecoveries(): Promise<WalletCoinRecovery[]> { + return await this.tx.coinRecoveries.getAll(); + } + + async getRefreshMeltConfirmation( + ref: Pick< + WalletRefreshMeltConfirmation, + "exchangeBaseUrl" | "refreshCommitment" + >, + ): Promise<WalletRefreshMeltConfirmation | undefined> { + return await this.tx.refreshMeltConfirmations.get([ + ref.exchangeBaseUrl, + ref.refreshCommitment, + ]); + } + + async upsertRefreshMeltConfirmation( + rec: WalletRefreshMeltConfirmation, + ): Promise<void> { + await this.tx.refreshMeltConfirmations.put(rec); + } + + async listAllRefreshMeltConfirmations(): Promise< + WalletRefreshMeltConfirmation[] + > { + return await this.tx.refreshMeltConfirmations.getAll(); + } + async getRecoupGroup( recoupGroupId: string, ): Promise<WalletRecoupGroup | undefined> { diff --git a/packages/taler-wallet-core/src/db/migration/converter.test.ts b/packages/taler-wallet-core/src/db/migration/converter.test.ts @@ -59,7 +59,7 @@ import { WithdrawalRecordType, WithdrawalGroupStatus, } from "../records.js"; -import { SQLITE_BASELINE_SCHEMA } from "../sqlite/schema.js"; +import { NATIVE_DATA_TABLES } from "../sqlite/schema.js"; import { convertWalletDb, DB_CONVERSION_BATCH_SIZE, @@ -1327,6 +1327,8 @@ test("converter: the copy plan covers every table in the schema", async () => { purchases: "purchases", deposit_groups: "depositGroups", refresh_groups: "refreshGroups", + coin_recoveries: "coinRecoveries", + refresh_melt_confirmations: "refreshMeltConfirmations", denom_loss_events: "denomLossEvents", peer_push_debit: "peerPushDebit", peer_push_credit: "peerPushCredit", @@ -1352,9 +1354,7 @@ test("converter: the copy plan covers every table in the schema", async () => { refund_items: "refundItems", }; - const tables = [ - ...SQLITE_BASELINE_SCHEMA.matchAll(/CREATE TABLE IF NOT EXISTS (\w+)/g), - ].map((m) => m[1]); + const tables = NATIVE_DATA_TABLES; assert.ok(tables.length >= 44, "schema parse failed"); const visited = new Set(Object.keys(report.copied)); diff --git a/packages/taler-wallet-core/src/db/migration/converter.ts b/packages/taler-wallet-core/src/db/migration/converter.ts @@ -411,6 +411,16 @@ const COPY_PLAN: CopyStep[][] = [ }, ), step( + "coinRecoveries", + (tx) => tx.listAllCoinRecoveries(), + (tx, r) => tx.upsertCoinRecovery(r), + ), + step( + "refreshMeltConfirmations", + (tx) => tx.listAllRefreshMeltConfirmations(), + (tx, r) => tx.upsertRefreshMeltConfirmation(r), + ), + step( "coinHistory", (tx) => tx.listAllCoinHistories(), (tx, r) => tx.upsertCoinHistory(r), diff --git a/packages/taler-wallet-core/src/db/records.ts b/packages/taler-wallet-core/src/db/records.ts @@ -461,6 +461,8 @@ export interface WalletRefreshGroup { * Ongoing refresh */ export interface WalletRefreshSession { + /** Original CS seed when recovering a refresh made by another wallet. */ + blindingSeed?: string; /** Once set, the melt may have reached the exchange; never change outputs. */ autoRefreshMeltStarted?: boolean; refreshGroupId: string; @@ -517,6 +519,26 @@ export interface WalletRefreshSession { // (legacy v1 refresh) } +/** Durable worklist; retained until every coin and residual refresh is handled. */ +export interface WalletCoinRecovery { + exchangeBaseUrl: string; + pending: string[]; + /** Fresh inputs retired by recovery; safe to refresh even after a restart. */ + claimed: string[]; + visited: string[]; + imported: string[]; + /** Newly recovered dormant coins, not allocated by a local operation. */ + quarantined: string[]; + residuals: Record<string, string>; +} + +/** Authenticated melt index, pinned before disclosing any refresh secrets. */ +export interface WalletRefreshMeltConfirmation { + exchangeBaseUrl: string; + refreshCommitment: string; + norevealIndex: number; +} + export const enum WithdrawalRecordType { BankManual = "bank-manual", BankIntegrated = "bank-integrated", diff --git a/packages/taler-wallet-core/src/db/sqlite/schema-migrations.test.ts b/packages/taler-wallet-core/src/db/sqlite/schema-migrations.test.ts @@ -689,3 +689,48 @@ test("DD71 migration preserves deadlines through closing and reopening", async ( cleanup(); } }); + +test("coin recovery stores are created when upgrading from master", async () => { + const { path, cleanup } = withTempDb(); + try { + let db = await openRaw(path); + await initSqliteWalletDb( + db, + schemaMigrations.filter((x) => x.version < 23), + ); + assert.deepStrictEqual( + await queryAll(db, "PRAGMA table_info(coin_recoveries)"), + [], + ); + await db.close(); + + db = await openRaw(path); + await initSqliteWalletDb(db); + const journalColumns = await queryAll( + db, + "PRAGMA table_info(coin_recoveries)", + ); + assert.ok( + journalColumns.some( + (x) => x.name === "exchange_base_url" && Number(x.pk) === 1, + ), + ); + const pinColumns = await queryAll( + db, + "PRAGMA table_info(refresh_melt_confirmations)", + ); + assert.ok( + pinColumns.some( + (x) => x.name === "refresh_commitment" && x.type === "BLOB", + ), + ); + assert.ok( + (await queryAll(db, "PRAGMA table_info(refresh_sessions)")).some( + (x) => x.name === "blinding_seed", + ), + ); + await db.close(); + } finally { + cleanup(); + } +}); diff --git a/packages/taler-wallet-core/src/db/sqlite/schema.ts b/packages/taler-wallet-core/src/db/sqlite/schema.ts @@ -85,7 +85,7 @@ * * Bump this when adding a migration to {@link schemaMigrations}. */ -export const SQLITE_SCHEMA_VERSION = 22; +export const SQLITE_SCHEMA_VERSION = 23; /** * Tables of the IndexedDB emulation, children before parents. @@ -265,7 +265,8 @@ export const BLOB_COLUMNS: Readonly<Record<string, readonly string[]>> = { "nonce_pub", "secret_seed", ], - refresh_sessions: ["session_public_seed"], + refresh_sessions: ["session_public_seed", "blinding_seed"], + refresh_melt_confirmations: ["refresh_commitment"], refund_items: ["coin_pub"], reserves: ["reserve_priv", "reserve_pub"], slates: [ @@ -1487,11 +1488,35 @@ export const schemaMigrations: SchemaMigration[] = [ "ALTER TABLE refresh_sessions ADD COLUMN auto_refresh_melt_started INTEGER CHECK (auto_refresh_melt_started IN (0, 1))", ], }, + { + version: 23, + name: "coin-recovery", + statements: [ + "ALTER TABLE refresh_sessions ADD COLUMN blinding_seed BLOB", + `CREATE TABLE coin_recoveries ( + exchange_base_url TEXT PRIMARY KEY NOT NULL, + pending TEXT NOT NULL, -- JSON: coin public keys + claimed TEXT NOT NULL, -- JSON: coin public keys + visited TEXT NOT NULL, -- JSON: coin public keys + imported TEXT NOT NULL, -- JSON: coin public keys + quarantined TEXT NOT NULL, -- JSON: coin public keys + residuals TEXT NOT NULL -- JSON: old coin public key to refresh group ID + )`, + `CREATE TABLE refresh_melt_confirmations ( + exchange_base_url TEXT NOT NULL, + refresh_commitment BLOB NOT NULL, + noreveal_index INTEGER NOT NULL CHECK (noreveal_index BETWEEN 0 AND 2), + PRIMARY KEY (exchange_base_url, refresh_commitment) + )`, + ], + }, ]; /** Native tables that contain wallet records (not schema bookkeeping). */ export const NATIVE_DATA_TABLES = [ - ...SQLITE_BASELINE_SCHEMA.matchAll(/CREATE TABLE IF NOT EXISTS (\w+)/g), -] - .map((m) => m[1]) - .filter((name) => !NON_DATA_TABLES.includes(name)); + ...[...SQLITE_BASELINE_SCHEMA.matchAll(/CREATE TABLE IF NOT EXISTS (\w+)/g)] + .map((m) => m[1]) + .filter((name) => !NON_DATA_TABLES.includes(name)), + "coin_recoveries", + "refresh_melt_confirmations", +]; diff --git a/packages/taler-wallet-core/src/db/sqlite/transaction.ts b/packages/taler-wallet-core/src/db/sqlite/transaction.ts @@ -116,6 +116,8 @@ import { WalletRecoupGroup, WalletRefreshGroup, WalletRefreshSession, + WalletCoinRecovery, + WalletRefreshMeltConfirmation, WalletSlate, WalletToken, WalletTransactionMeta, @@ -260,6 +262,8 @@ const SQLITE_MIGRATION_TABLES: Record<WalletDbMigrationStore, string> = { coins: "coins", planchets: "planchets", refreshSessions: "refresh_sessions", + coinRecoveries: "coin_recoveries", + refreshMeltConfirmations: "refresh_melt_confirmations", coinHistory: "coin_history", coinAvailability: "coin_availability", refundGroups: "refund_groups", @@ -5300,6 +5304,9 @@ export class SqliteWalletTransaction implements WalletDbTransaction { coinIndex: num(row.coin_index), amountRefreshOutput: dbAmount(row.amount_refresh_output), newDenoms: dbToJson(row.new_denoms), + ...(row.blinding_seed != null + ? { blindingSeed: dbToCrock(row.blinding_seed) } + : {}), ...(row.session_public_seed != null ? { sessionPublicSeed: dbToCrock(row.session_public_seed) } : undefined), @@ -5332,8 +5339,8 @@ export class SqliteWalletTransaction implements WalletDbTransaction { `INSERT INTO refresh_sessions ( refresh_group_id, coin_index, session_public_seed, refresh_protocol_version, amount_refresh_output, new_denoms, - noreveal_index, last_error, auto_refresh_melt_started - ) VALUES ($id, $idx, $seed, $rpv, $amt, $newDenoms, $nri, $err, $arms) + noreveal_index, last_error, auto_refresh_melt_started, blinding_seed + ) VALUES ($id, $idx, $seed, $rpv, $amt, $newDenoms, $nri, $err, $arms, $blindingSeed) ON CONFLICT(refresh_group_id, coin_index) DO UPDATE SET session_public_seed = excluded.session_public_seed, refresh_protocol_version = excluded.refresh_protocol_version, @@ -5341,11 +5348,13 @@ export class SqliteWalletTransaction implements WalletDbTransaction { new_denoms = excluded.new_denoms, noreveal_index = excluded.noreveal_index, last_error = excluded.last_error, - auto_refresh_melt_started = excluded.auto_refresh_melt_started`, + auto_refresh_melt_started = excluded.auto_refresh_melt_started, + blinding_seed = excluded.blinding_seed`, { id: rec.refreshGroupId, idx: rec.coinIndex, arms: boolToDb(rec.autoRefreshMeltStarted), + blindingSeed: optCrockToDb(rec.blindingSeed), seed: optCrockToDb(rec.sessionPublicSeed), rpv: rec.refreshProtocolVersion ?? null, amt: rec.amountRefreshOutput, @@ -5383,6 +5392,105 @@ export class SqliteWalletTransaction implements WalletDbTransaction { return rows.map((r) => this.rowToRefreshSession(r)); } + // ----------------------------------------------------- coin recovery + + private rowToCoinRecovery(row: ResultRow): WalletCoinRecovery { + return { + exchangeBaseUrl: str(row.exchange_base_url), + pending: dbToJson(row.pending), + claimed: dbToJson(row.claimed), + visited: dbToJson(row.visited), + imported: dbToJson(row.imported), + quarantined: dbToJson(row.quarantined), + residuals: dbToJson(row.residuals), + }; + } + + async getCoinRecovery( + exchangeBaseUrl: string, + ): Promise<WalletCoinRecovery | undefined> { + const row = await this.first( + "SELECT * FROM coin_recoveries WHERE exchange_base_url = $url", + { url: exchangeBaseUrl }, + ); + return row ? this.rowToCoinRecovery(row) : undefined; + } + + async upsertCoinRecovery(rec: WalletCoinRecovery): Promise<void> { + await this.run( + `INSERT INTO coin_recoveries ( + exchange_base_url, pending, claimed, visited, imported, quarantined, residuals + ) VALUES ($url, $pending, $claimed, $visited, $imported, $quarantined, $residuals) + ON CONFLICT(exchange_base_url) DO UPDATE SET + pending = excluded.pending, claimed = excluded.claimed, + visited = excluded.visited, imported = excluded.imported, + quarantined = excluded.quarantined, residuals = excluded.residuals`, + { + url: rec.exchangeBaseUrl, + pending: jsonToDb(rec.pending), + claimed: jsonToDb(rec.claimed), + visited: jsonToDb(rec.visited), + imported: jsonToDb(rec.imported), + quarantined: jsonToDb(rec.quarantined), + residuals: jsonToDb(rec.residuals), + }, + ); + } + + async listAllCoinRecoveries(): Promise<WalletCoinRecovery[]> { + return (await this.all("SELECT * FROM coin_recoveries")).map((row) => + this.rowToCoinRecovery(row), + ); + } + + private rowToRefreshMeltConfirmation( + row: ResultRow, + ): WalletRefreshMeltConfirmation { + return { + exchangeBaseUrl: str(row.exchange_base_url), + refreshCommitment: dbToCrock(row.refresh_commitment), + norevealIndex: num(row.noreveal_index), + }; + } + + async getRefreshMeltConfirmation( + ref: Pick< + WalletRefreshMeltConfirmation, + "exchangeBaseUrl" | "refreshCommitment" + >, + ): Promise<WalletRefreshMeltConfirmation | undefined> { + const row = await this.first( + `SELECT * FROM refresh_melt_confirmations + WHERE exchange_base_url = $url AND refresh_commitment = $rc`, + { url: ref.exchangeBaseUrl, rc: crockToDb(ref.refreshCommitment) }, + ); + return row ? this.rowToRefreshMeltConfirmation(row) : undefined; + } + + async upsertRefreshMeltConfirmation( + rec: WalletRefreshMeltConfirmation, + ): Promise<void> { + await this.run( + `INSERT INTO refresh_melt_confirmations (exchange_base_url, refresh_commitment, noreveal_index) + VALUES ($url, $rc, $idx) + ON CONFLICT(exchange_base_url, refresh_commitment) DO UPDATE SET + noreveal_index = excluded.noreveal_index`, + { + url: rec.exchangeBaseUrl, + rc: crockToDb(rec.refreshCommitment), + idx: rec.norevealIndex, + }, + ); + } + + async listAllRefreshMeltConfirmations(): Promise< + WalletRefreshMeltConfirmation[] + > { + return (await this.all("SELECT * FROM refresh_melt_confirmations")).map( + (row) => this.rowToRefreshMeltConfirmation(row), + ); + } + // ----------------------------------------------------- recoup groups private rowToRecoupGroup(row: ResultRow): WalletRecoupGroup { diff --git a/packages/taler-wallet-core/src/db/testing/conformance-cases.ts b/packages/taler-wallet-core/src/db/testing/conformance-cases.ts @@ -83,6 +83,8 @@ import { WalletRecoupGroup, WalletRefreshGroup, WalletRefreshSession, + WalletCoinRecovery, + WalletRefreshMeltConfirmation, WalletSlate, WalletToken, WalletWithdrawalGroup, @@ -3895,6 +3897,7 @@ export const conformanceCases: ConformanceCase[] = [ const fresh = makeRefreshSession("rs-melt", 0); const melted = makeRefreshSession("rs-melt", 1); melted.sessionPublicSeed = ckh("seed-melt"); + melted.blindingSeed = ck("foreign-cs-seed"); melted.refreshProtocolVersion = 32; melted.norevealIndex = 2; melted.autoRefreshMeltStarted = true; @@ -3919,6 +3922,56 @@ export const conformanceCases: ConformanceCase[] = [ }, { + name: "coin recovery: journals and melt confirmations are isolated by exchange and commitment", + async run(t, runner) { + const journal: WalletCoinRecovery = { + exchangeBaseUrl: "https://recovery.example/", + pending: [ck("pending")], + claimed: [ck("claimed")], + visited: [ck("visited")], + imported: [ck("imported")], + quarantined: [ck("quarantined")], + residuals: { [ck("residual")]: "residual-refresh-group" }, + }; + const other = { ...journal, exchangeBaseUrl: "https://other.example/" }; + const pins: WalletRefreshMeltConfirmation[] = [ + { + exchangeBaseUrl: journal.exchangeBaseUrl, + refreshCommitment: ckh("melt-a"), + norevealIndex: 0, + }, + { + exchangeBaseUrl: other.exchangeBaseUrl, + refreshCommitment: ckh("melt-a"), + norevealIndex: 1, + }, + { + exchangeBaseUrl: journal.exchangeBaseUrl, + refreshCommitment: ckh("melt-b"), + norevealIndex: 2, + }, + ]; + await runner.runReadWriteTx(async (tx) => { + t.equal(await tx.getCoinRecovery(journal.exchangeBaseUrl), undefined); + t.equal(await tx.getRefreshMeltConfirmation(pins[0]), undefined); + await tx.upsertCoinRecovery(journal); + await tx.upsertCoinRecovery(other); + for (const pin of pins) await tx.upsertRefreshMeltConfirmation(pin); + }); + journal.pending = [ck("next")]; + await runner.runReadWriteTx((tx) => tx.upsertCoinRecovery(journal)); + await runner.runReadWriteTx(async (tx) => { + t.deepEqual(await tx.getCoinRecovery(journal.exchangeBaseUrl), journal); + t.deepEqual(await tx.getCoinRecovery(other.exchangeBaseUrl), other); + t.equal((await tx.listAllCoinRecoveries()).length, 2); + for (const pin of pins) + t.deepEqual(await tx.getRefreshMeltConfirmation(pin), pin); + t.equal((await tx.listAllRefreshMeltConfirmations()).length, 3); + }); + }, + }, + + { name: "recoup group: round trip, by exchange and active range", async run(t, runner) { const rc = makeRecoupGroup("rc-1", "https://rex/"); diff --git a/packages/taler-wallet-core/src/db/transaction.ts b/packages/taler-wallet-core/src/db/transaction.ts @@ -63,6 +63,8 @@ import { WalletReserve, WalletRefreshGroup, WalletRefreshSession, + WalletCoinRecovery, + WalletRefreshMeltConfirmation, WalletWithdrawalGroup, WalletPlanchet, WalletDonationSummary, @@ -185,6 +187,8 @@ export type WalletDbMigrationStore = | "coins" | "planchets" | "refreshSessions" + | "coinRecoveries" + | "refreshMeltConfirmations" | "coinHistory" | "coinAvailability" | "refundGroups" @@ -955,6 +959,33 @@ export interface WalletDbTransaction { /** List every refresh session, including orphaned legacy rows. */ listAllRefreshSessions(): Promise<WalletRefreshSession[]>; + /** Get the resumable recovery worklist for an exchange. */ + getCoinRecovery( + exchangeBaseUrl: string, + ): Promise<WalletCoinRecovery | undefined>; + + /** Replace the recovery worklist for an exchange. */ + upsertCoinRecovery(rec: WalletCoinRecovery): Promise<void>; + + /** List recovery worklists for database conversion. */ + listAllCoinRecoveries(): Promise<WalletCoinRecovery[]>; + + /** Get the authenticated index pinned for this exchange and commitment. */ + getRefreshMeltConfirmation( + ref: Pick< + WalletRefreshMeltConfirmation, + "exchangeBaseUrl" | "refreshCommitment" + >, + ): Promise<WalletRefreshMeltConfirmation | undefined>; + + /** Store a melt confirmation after checking any existing pin. */ + upsertRefreshMeltConfirmation( + rec: WalletRefreshMeltConfirmation, + ): Promise<void>; + + /** List pinned confirmations for database conversion. */ + listAllRefreshMeltConfirmations(): Promise<WalletRefreshMeltConfirmation[]>; + /** Get a recoup group by ID. */ getRecoupGroup(recoupGroupId: string): Promise<WalletRecoupGroup | undefined>;