Observed
On a restored mainnet wallet (Android host, kotlin-sdk v42int AAR line): a historical send that spent a single denominated CoinJoin input (10.0001 DASH, the largest denomination) paying ~3.4 with ~6.6 change back displays as a 10 DASH send. The stored transactions record's net amount equals the gross input value.
Mechanism
The restore deep scan's ownership/watch set does not include the CoinJoin derivation chains. Change from a CoinJoin-funded send goes to a CoinJoin-chain address, which the scan attributes as foreign, so the record is born with net = −(sum of inputs) instead of −(payment + fee). The record's direction class is correct (OUTGOING), so class-level reconciliation on the host never revisits it — the wrong net is durable.
Relationship to the known restore persistence gap
This is the display variant of the same ownership gap already tracked for the pending persistence fix: the store also drops unspent CoinJoin-change outputs on restore (the balance variant). Both variants should close at the same layer — the restore scan's ownership set must include CoinJoin derivations — plus a heal/backfill pass for already-restored stores whose records were born with gross nets. Note the Android host ships a class-level reattribution walker that persists its corrections durably; an SDK-side heal needs to compose with (not fight) those persisted host corrections.
Acceptance
After restore-from-seed of a wallet with CoinJoin history:
- sends funded by denominated inputs net to payment + fee (change attributed owned);
- unspent CoinJoin change is present in balance;
- a reconciliation pass corrects born-wrong nets in existing restored stores.
Coordination note: #4406 is currently active on the swept-transaction persistence seam — worth aligning so the two don't collide.
Observed
On a restored mainnet wallet (Android host, kotlin-sdk v42int AAR line): a historical send that spent a single denominated CoinJoin input (10.0001 DASH, the largest denomination) paying ~3.4 with ~6.6 change back displays as a 10 DASH send. The stored
transactionsrecord's net amount equals the gross input value.Mechanism
The restore deep scan's ownership/watch set does not include the CoinJoin derivation chains. Change from a CoinJoin-funded send goes to a CoinJoin-chain address, which the scan attributes as foreign, so the record is born with net = −(sum of inputs) instead of −(payment + fee). The record's direction class is correct (OUTGOING), so class-level reconciliation on the host never revisits it — the wrong net is durable.
Relationship to the known restore persistence gap
This is the display variant of the same ownership gap already tracked for the pending persistence fix: the store also drops unspent CoinJoin-change outputs on restore (the balance variant). Both variants should close at the same layer — the restore scan's ownership set must include CoinJoin derivations — plus a heal/backfill pass for already-restored stores whose records were born with gross nets. Note the Android host ships a class-level reattribution walker that persists its corrections durably; an SDK-side heal needs to compose with (not fight) those persisted host corrections.
Acceptance
After restore-from-seed of a wallet with CoinJoin history:
Coordination note: #4406 is currently active on the swept-transaction persistence seam — worth aligning so the two don't collide.