View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011978 | Taler | wallet-core | public | 2026-11-09 09:13 | 2026-11-09 09:13 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | always |
| Status | new | Resolution | open | ||
| Summary | 0011978: wallet-core: a peer-to-peer credit blocked on KYC sets no incoming-kyc balance flag, so wallets cannot tell it from a slow transfer (patch + tests attached) | ||||
| Description | `getBalancesInternal` raises `incoming-kyc` for a withdrawal blocked on KYC and `outgoing-kyc` for a deposit blocked on KYC, but raises nothing for a **peer-push or peer-pull credit** blocked on KYC. The amount is added to `pendingIncoming` and that is all, so at the balance level a transfer that is waiting for the exchange and one that is waiting for the *user* are indistinguishable. That is the wallet-core half of bug 11914 ("withdrawal runs into KYC ... but iOS wallet gives no chance to user to DO the KYC"): a wallet that wants to badge the balance row cannot, because the flag it would badge on is never set. Where ----- `packages/taler-wallet-core/src/balance.ts`, in `getBalancesInternal`: * **withdrawals, :719-735** -- `PendingKyc`, `PendingBalanceKyc`, `PendingBalanceKycInit`, `SuspendedKyc`, `SuspendedBalanceKyc`, `SuspendedBalanceKycInit` all call `setFlagIncomingKyc`. * **deposit groups, :943-951** -- the six `*DepositKyc*` / `*AggregateKyc` statuses call `setFlagOutgoingKyc`, in a switch of their own. * **peer push credits, :818-844** -- `PendingMergeKycRequired`, `PendingBalanceKycRequired`, `PendingBalanceKycInit` and their `Suspended*` twins fall into the same arm as the ordinary `PendingMerge` and call only `addPendingIncoming`. No flag. * **peer pull credits, :846-869** -- same, with `PendingReady` and `PendingCreatePurse` in the same arm. Observed -------- Two `taler-wallet-cli` wallets against an exchange with a `MERGE` rule at KUDOS:2. Wallet A pushes a KUDOS:2.5 purse, wallet B accepts it: getTransactions -> peer-push-credit pending / kyc KUDOS:2.5 kycUrl=<set> getBalances -> pendingIncoming = KUDOS:2.5 flags = [] The transaction knows it needs KYC and carries the URL. The balance does not say so. Patch ----- Attached (`wallet-core-incoming-kyc-flag.diff`), against c7511e514. It adds a flag-only `switch` to each of the two loops, in the same shape the deposit loop already uses, and two unit tests in `balance.test.ts`. Verified on the same wallet database, same commit, one build with the patch and one without: without: pendingIncoming = KUDOS:2.5 flags = [] with: pendingIncoming = KUDOS:2.5 flags = ['incoming-kyc'] `tsc --noEmit` clean, `prettier --check` clean, `node --test lib/balance.test.js` 13/13. The new test fails on unpatched `balance.ts` ("not ok 12") and passes with it, so it pins the behaviour rather than describing it. Judgement calls worth a reviewer's eye -------------------------------------- * `*BalanceKycInit` is included, to match the withdrawal loop, which already flags `PendingBalanceKycInit`. Note that iOS deliberately does *not* show its KYC button in the transaction's `kyc-init` state (bug 10214), so a wallet may want to badge the balance while the button is still hidden; the two are different questions and the flag answers the first. * `PendingWithdrawing` is left alone: the balance there is covered by the internal withdrawal group, which raises its own flag if it needs to. | ||||
| Tags | No tags attached. | ||||
| Attached Files | wallet-core-incoming-kyc-flag.diff (6,946 bytes)
diff --git a/packages/taler-wallet-core/src/balance.test.ts b/packages/taler-wallet-core/src/balance.test.ts
index f3576906a..66261e1ee 100644
--- a/packages/taler-wallet-core/src/balance.test.ts
+++ b/packages/taler-wallet-core/src/balance.test.ts
@@ -17,6 +17,7 @@ import { test } from "node:test";
import {
Amounts,
+ BalanceFlag,
RefreshReason,
ScopeType,
stringifyScopeInfo,
@@ -29,6 +30,8 @@ import {
ExchangeEntryDbRecordStatus,
ExchangeEntryDbUpdateStatus,
DepositOperationStatus,
+ PeerPushCreditStatus,
+ PeerPullPaymentCreditStatus,
PurchaseStatus,
RefreshOperationStatus,
WalletCoinAvailability,
@@ -36,6 +39,8 @@ import {
WalletExchangeEntry,
WalletRefreshGroup,
WalletDepositGroup,
+ WalletPeerPushCredit,
+ WalletPeerPullCredit,
} from "./db/records.js";
import { WalletDbTransaction } from "./db/transaction.js";
import { WalletExecutionContext } from "./wallet.js";
@@ -628,3 +633,81 @@ test("active purchases share one deduplicated coin lookup", async () => {
assert.strictEqual(byUrl.get(exchangeA)?.pendingOutgoing, "TESTKUDOS:4");
assert.strictEqual(byUrl.get(exchangeB)?.pendingOutgoing, "TESTKUDOS:2");
});
+
+test("a peer credit blocked on KYC raises the incoming-kyc flag", async () => {
+ // Without the flag a wallet cannot tell, at the balance level, a transfer
+ // that is waiting for the exchange from one that is waiting for the user.
+ // Both show up only as pendingIncoming, which is what left the iOS wallet
+ // showing a bare "Pending incoming" row for a receipt the user had to act
+ // on (bugs.taler.net/n/11914). Withdrawals and deposits have always
+ // raised their flag; peer credits did not.
+ const exchange = "https://peer-kyc.example/";
+ for (const status of [
+ PeerPushCreditStatus.PendingMergeKycRequired,
+ PeerPushCreditStatus.PendingBalanceKycRequired,
+ PeerPushCreditStatus.PendingBalanceKycInit,
+ PeerPushCreditStatus.SuspendedMergeKycRequired,
+ ]) {
+ const { wex, tx } = makeBalanceContext([makeExchange(exchange)]);
+ tx.getActivePeerPushCredits = async () => [
+ {
+ exchangeBaseUrl: exchange,
+ estimatedAmountEffective: "TESTKUDOS:2.5",
+ status,
+ } as unknown as WalletPeerPushCredit,
+ ];
+ const balances = await getBalancesInsideTransaction(wex, tx);
+ const b = balances.balances.find(
+ (x) => x.scopeInfo.type === ScopeType.Exchange,
+ );
+ assert.strictEqual(b?.pendingIncoming, "TESTKUDOS:2.5", `push ${status}`);
+ assert.ok(
+ b?.flags.includes(BalanceFlag.IncomingKyc),
+ `push ${status} should raise incoming-kyc, got ${JSON.stringify(b?.flags)}`,
+ );
+ }
+
+ for (const status of [
+ PeerPullPaymentCreditStatus.PendingMergeKycRequired,
+ PeerPullPaymentCreditStatus.PendingBalanceKycRequired,
+ PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired,
+ ]) {
+ const { wex, tx } = makeBalanceContext([makeExchange(exchange)]);
+ tx.getActivePeerPullCredits = async () => [
+ {
+ exchangeBaseUrl: exchange,
+ amount: "TESTKUDOS:2.5",
+ estimatedAmountEffective: "TESTKUDOS:2.5",
+ status,
+ } as unknown as WalletPeerPullCredit,
+ ];
+ const balances = await getBalancesInsideTransaction(wex, tx);
+ const b = balances.balances.find(
+ (x) => x.scopeInfo.type === ScopeType.Exchange,
+ );
+ assert.ok(
+ b?.flags.includes(BalanceFlag.IncomingKyc),
+ `pull ${status} should raise incoming-kyc, got ${JSON.stringify(b?.flags)}`,
+ );
+ }
+});
+
+test("a peer credit that is merely pending raises no flag", async () => {
+ // The other half of the claim: the flag has to mean "waiting for you", so
+ // an ordinary pending merge must not set it.
+ const exchange = "https://peer-plain.example/";
+ const { wex, tx } = makeBalanceContext([makeExchange(exchange)]);
+ tx.getActivePeerPushCredits = async () => [
+ {
+ exchangeBaseUrl: exchange,
+ estimatedAmountEffective: "TESTKUDOS:2.5",
+ status: PeerPushCreditStatus.PendingMerge,
+ } as unknown as WalletPeerPushCredit,
+ ];
+ const balances = await getBalancesInsideTransaction(wex, tx);
+ const b = balances.balances.find(
+ (x) => x.scopeInfo.type === ScopeType.Exchange,
+ );
+ assert.strictEqual(b?.pendingIncoming, "TESTKUDOS:2.5");
+ assert.deepStrictEqual(b?.flags, []);
+});
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index b13a9b3d1..0a6631054 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -817,6 +817,27 @@ export async function getBalancesInsideTransaction(
const peerPushCredits = await tx.getActivePeerPushCredits();
for (const rec of peerPushCredits) {
+ // A separate switch for the flag, the way the deposit-group loop below
+ // does it: the pending-amount cases and the needs-the-user cases are
+ // different questions and overlap only partly.
+ //
+ // Without this, a wallet cannot tell the two apart at the balance
+ // level. The amount shows up as pendingIncoming either way, so a
+ // transfer that is waiting for the exchange and one that is waiting
+ // for the user to pass a legitimization check render identically, and
+ // the only way to find out which is to open the transaction.
+ switch (rec.status) {
+ case PeerPushCreditStatus.PendingBalanceKycInit:
+ case PeerPushCreditStatus.PendingBalanceKycRequired:
+ case PeerPushCreditStatus.PendingMergeKycRequired:
+ case PeerPushCreditStatus.SuspendedBalanceKycInit:
+ case PeerPushCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPushCreditStatus.SuspendedMergeKycRequired:
+ await balanceStore.setFlagIncomingKyc(
+ Amounts.currencyOf(rec.estimatedAmountEffective),
+ rec.exchangeBaseUrl,
+ );
+ }
switch (rec.status) {
case PeerPushCreditStatus.PendingMerge:
case PeerPushCreditStatus.PendingBalanceKycInit:
@@ -845,6 +866,18 @@ export async function getBalancesInsideTransaction(
const peerPullCredits = await tx.getActivePeerPullCredits();
for (const rec of peerPullCredits) {
+ switch (rec.status) {
+ case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
+ case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
+ case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
+ case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
+ await balanceStore.setFlagIncomingKyc(
+ Amounts.currencyOf(rec.amount),
+ rec.exchangeBaseUrl,
+ );
+ }
switch (rec.status) {
case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
| ||||