Repository navigation
platform-wallet: record DashPay payments per output, not per transaction #5601
Description
TL;DR: The wallet stores at most one DashPay payment per transaction for each identity, so a second payment in the same transaction (from another contact, or in the other direction) is silently not recorded.
User story
As a DashPay user, I want every payment a contact sends me to appear in my history, even when it arrives in the same transaction as another payment.
As a wallet app developer, I want stable per-payment keys, so I can attach my own data (first-seen time, notes) to one payment without it colliding with another in the same transaction.
Scenario
Base flow
- One transaction carries two DashPay legs for the same identity. Examples: two contacts paid it in a single batched transaction, or it pays a contact and also receives from another contact.
- The background sync records incoming payments, and sent payments are recorded at send time or reconstructed from history.
Actual behavior
Payments are stored in a map keyed by the bare transaction id. Whichever leg is recorded first wins, and later legs for that transaction are skipped, by design, so as not to overwrite. The second payment never appears, and its amount is missing from the contact's history.
Expected behavior
Each leg is recorded under its own key: a stable per-output or per-(contact, direction) key. All legs appear, and re-running the sync stays idempotent.
Detailed discussion
On v5.1-dev @ 575d961fe0:
DashPayState::payments: BTreeMap<String, PaymentEntry>is keyed by txid (state/managed_identity/dashpay.rs#L90). It is written only throughManagedIdentity::record_dashpay_payment(tx_id, …)(state/managed_identity/identity_ops.rs#L147-L185).- The incoming totals are computed per
(owner, contact, txid)and then recorded undertxidalone. A guard skips any txid already present (network/payments.rs#L1153-L1190, guard at #L1164). Sent reconstruction has the same guard (#L902). - The doc comment and the test around #L2995-L3020 pin the behaviour: an existing
Sententry for a txid suppresses an incoming total for the same transaction.
Likelihood: low. Batched multi-contact transactions are uncommon. Impact: a payment silently missing from history, with no error. That is why this is filed as a correctness gap rather than a feature.
Downstream: Dash Evo Tool used to record incoming payments under txid:vout. It now has to reconcile its older composite keys with the wallet's bare-txid keys and let the older entry shadow its twin (src/wallet_backend/dashpay_snapshot.rs::payments, marked TODO(upstream): per-output payment keys).
Proposed change
- Key entries by a composite that cannot collide within a transaction, for example
"{txid}:{vout}"for received outputs and"{txid}:sent:{contact}"for sent legs. Alternatively, make the mapBTreeMap<PaymentKey, PaymentEntry>withPaymentKey { txid, counterparty, direction, vout: Option<u32> }. - Keep
PaymentEntryunchanged, and addtxid()/vout()accessors on the key. - Migrate stored rows: a bare-txid key becomes the composite key of its single leg. Keep reading bare keys for one release.
- The idempotency guard then checks the composite key, so re-runs still never overwrite.
Prior work
- An ambiguous DashPay payment broadcast records no payment entry and returns no txid #4762: an ambiguous broadcast records no payment entry and returns no txid. A different gap in the same payment history.
- fix(platform-wallet): DashPay payment reconcile still stores under the wallet-manager write guard (stage/unstage split) #4609: payment reconcile persists under the wallet-manager write guard. It touches the same write paths, so the two should be coordinated.
- feat(platform-wallet): persist DashPay payment history through the persister callback #4326: persisted DashPay payment history through the persister callback, introducing the txid-keyed overlay this would change.
Attribution
🤖 Co-authored by Claudius the Magnificent AI Agent
Activity