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:
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.