taler-typescript-core

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

commit c7a779aeacd341a94a8bf4c81c0dbcb0dc1c7485
parent 31bcff25a7359f90db1ee469640c2c7665c79667
Author: Florian Dold <florian@dold.me>
Date:   Wed, 18 Mar 2026 19:28:45 +0100

wallet-core: improve deposit account pub heuristic

Fixes https://bugs.taler.net/n/11240

Diffstat:
Mpackages/taler-wallet-core/src/db.ts | 14++++++++++++++
Mpackages/taler-wallet-core/src/deposits.ts | 180+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mpackages/taler-wallet-core/src/wallet.ts | 5++++-
3 files changed, 131 insertions(+), 68 deletions(-)

diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -818,6 +818,20 @@ export interface ExchangeEntryRecord { currentMergeReserveRowId?: number; /** + * Current account private key. The corresponding public + * key is used as the merchant public key in deposits. + * + * When unset or reset, we use a heuristic to find an + * account priv/pub that likely already has KYC auth. + */ + currentAccountPriv?: string; + + /** + * @see currentAccountPriv + */ + currentAccountPub?: string; + + /** * Defaults to false. */ peerPaymentsDisabled?: boolean; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -112,8 +112,11 @@ import { DepositTrackingInfo, RefreshOperationStatus, WalletDbAllStoresReadOnlyTransaction, + WalletDbAllStoresReadWriteTransaction, WalletDbReadWriteTransaction, + WithdrawalGroupRecord, WithdrawalGroupStatus, + WithdrawalRecordType, timestampAbsoluteFromDb, timestampPreciseFromDb, timestampPreciseToDb, @@ -960,11 +963,6 @@ async function refundDepositGroup( * If done, mark the deposit transaction as aborted. * * Otherwise continue waiting. - * - * FIXME: Wait for the refresh group notifications instead of periodically - * checking the refresh group status. - * FIXME: This is just one transaction, can't we do this in the initial - * transaction of processDepositGroup? */ async function waitForRefreshOnDepositGroup( wex: WalletExecutionContext, @@ -1158,77 +1156,122 @@ async function processDepositGroupPendingKyc( }); } -/** - * Finds the reserve key pair of the most recent withdrawal - * with the given exchange. - * Returns undefined if no such withdrawal exists. - */ -async function getLastWithdrawalKeyPair( - wex: WalletExecutionContext, +async function tryFindAccountKeypair( + tx: WalletDbAllStoresReadWriteTransaction, exchangeBaseUrl: string, ): Promise<EddsaKeyPairStrings | undefined> { let candidateTimestamp: AbsoluteTime | undefined = undefined; let candidateRes: EddsaKeyPairStrings | undefined = undefined; - await wex.db.runAllStoresReadOnlyTx({}, async (tx) => { - const withdrawalRecs = - await tx.withdrawalGroups.indexes.byExchangeBaseUrl.getAll( - exchangeBaseUrl, - ); - for (const rec of withdrawalRecs) { - if (!rec.timestampFinish) { - continue; - } - const currTimestamp = timestampAbsoluteFromDb(rec.timestampFinish); - if ( - candidateTimestamp == null || - AbsoluteTime.cmp(currTimestamp, candidateTimestamp) > 0 - ) { - candidateTimestamp = currTimestamp; - candidateRes = { - priv: rec.reservePriv, - pub: rec.reservePub, - }; - } + const allWithdrawalRecs = + await tx.withdrawalGroups.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl); + // We only consider withdrawals that were actual + // withdrawals from a bank account. + const withdrawalRecs: WithdrawalGroupRecord[] = []; + for (const rec of allWithdrawalRecs) { + switch (rec.wgInfo.withdrawalType) { + case WithdrawalRecordType.BankIntegrated: + case WithdrawalRecordType.BankManual: + withdrawalRecs.push(rec); + break; } + } + for (const rec of withdrawalRecs) { + if (!rec.timestampFinish) { + continue; + } + const currTimestamp = timestampAbsoluteFromDb(rec.timestampFinish); + if ( + candidateTimestamp == null || + AbsoluteTime.cmp(currTimestamp, candidateTimestamp) > 0 + ) { + candidateTimestamp = currTimestamp; + candidateRes = { + priv: rec.reservePriv, + pub: rec.reservePub, + }; + } + } - if (candidateRes) { - // We already found a good candidate. - return; + if (candidateRes) { + // We already found a good candidate. + return candidateRes; + } + + // No good candidate, try finding a withdrawal group that's at + // least currently pending, so it might be completed in the future. + for (const rec of withdrawalRecs) { + switch (rec.status) { + case WithdrawalGroupStatus.PendingBalanceKyc: + case WithdrawalGroupStatus.PendingBalanceKycInit: + case WithdrawalGroupStatus.PendingKyc: + case WithdrawalGroupStatus.PendingQueryingStatus: + case WithdrawalGroupStatus.PendingReady: + case WithdrawalGroupStatus.PendingRegisteringBank: + case WithdrawalGroupStatus.PendingWaitConfirmBank: + case WithdrawalGroupStatus.SuspendedBalanceKyc: + case WithdrawalGroupStatus.SuspendedBalanceKycInit: + case WithdrawalGroupStatus.SuspendedKyc: + case WithdrawalGroupStatus.SuspendedQueryingStatus: + case WithdrawalGroupStatus.SuspendedReady: + case WithdrawalGroupStatus.SuspendedRegisteringBank: + case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + } + const currTimestamp = timestampAbsoluteFromDb(rec.timestampStart); + if ( + candidateTimestamp == null || + AbsoluteTime.cmp(currTimestamp, candidateTimestamp) > 0 + ) { + candidateTimestamp = currTimestamp; + candidateRes = { + priv: rec.reservePriv, + pub: rec.reservePub, + }; } + } + return candidateRes; +} - // No good candidate, try finding a withdrawal group that's at - // least currently pending, so it might be completed in the future. - for (const rec of withdrawalRecs) { - switch (rec.status) { - case WithdrawalGroupStatus.PendingBalanceKyc: - case WithdrawalGroupStatus.PendingBalanceKycInit: - case WithdrawalGroupStatus.PendingKyc: - case WithdrawalGroupStatus.PendingQueryingStatus: - case WithdrawalGroupStatus.PendingReady: - case WithdrawalGroupStatus.PendingRegisteringBank: - case WithdrawalGroupStatus.PendingWaitConfirmBank: - case WithdrawalGroupStatus.SuspendedBalanceKyc: - case WithdrawalGroupStatus.SuspendedBalanceKycInit: - case WithdrawalGroupStatus.SuspendedKyc: - case WithdrawalGroupStatus.SuspendedQueryingStatus: - case WithdrawalGroupStatus.SuspendedReady: - case WithdrawalGroupStatus.SuspendedRegisteringBank: - case WithdrawalGroupStatus.SuspendedWaitConfirmBank: - } - const currTimestamp = timestampAbsoluteFromDb(rec.timestampStart); - if ( - candidateTimestamp == null || - AbsoluteTime.cmp(currTimestamp, candidateTimestamp) > 0 - ) { - candidateTimestamp = currTimestamp; - candidateRes = { - priv: rec.reservePriv, - pub: rec.reservePub, - }; - } +/** + * Find a suitable account key pair for deposit + * or create a new one if required. + */ +async function provideDepositAccountKeypair( + wex: WalletExecutionContext, + exchangeBaseUrl: string, +): Promise<EddsaKeyPairStrings> { + const existingPair = await wex.runLegacyWalletDbTx(async (tx, wtx) => { + const exchange = await tx.exchanges.get(exchangeBaseUrl); + if (!exchange) { + throw Error("exchange for deposit not found anymore"); } + if (exchange.currentAccountPriv && exchange.currentAccountPriv) { + return { + priv: exchange.currentAccountPriv, + pub: exchange.currentAccountPriv, + }; + } + const res = await tryFindAccountKeypair(tx, exchangeBaseUrl); + if (res != null) { + exchange.currentAccountPriv = res.priv; + exchange.currentAccountPub = res.pub; + await tx.exchanges.put(exchange); + } + return res; }); - return candidateRes; + if (existingPair) { + return existingPair; + } + const newPair = await wex.cryptoApi.createEddsaKeypair({}); + await wex.runLegacyWalletDbTx(async (tx, wtx) => { + const exchange = await tx.exchanges.get(exchangeBaseUrl); + if (!exchange) { + throw Error("exchange for deposit not found anymore"); + } + exchange.currentAccountPriv = newPair.priv; + exchange.currentAccountPub = newPair.pub; + await tx.exchanges.put(exchange); + }); + return newPair; } /** @@ -2087,7 +2130,10 @@ export async function createDepositGroup( // Withdrawals from p2p reserves do not count/help. // We should also consider using the key pair from the last successful // deposit. - const res = await getLastWithdrawalKeyPair(wex, coins[0].exchangeBaseUrl); + const res = await provideDepositAccountKeypair( + wex, + coins[0].exchangeBaseUrl, + ); if (res) { logger.info( `reusing reserve pub ${res.pub} from last withdrawal to ${coins[0].exchangeBaseUrl}`, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -479,7 +479,10 @@ export interface WalletExecutionContext { * Uses the legacy transaction that only works with IndexedDB. */ runLegacyWalletDbTx<T>( - f: (tx: WalletDbAllStoresReadWriteTransaction) => Promise<T>, + f: ( + tx: WalletDbAllStoresReadWriteTransaction, + wtx: WalletDbTransaction, + ) => Promise<T>, ): Promise<T>; /** * Start a database transaction.