Repository navigation
platform-wallet: a transaction restored as a stand-in overwrites the stored row's amount and direction when its context changes #5416
Description
Expected Behavior
A transaction row a persister already stores keeps its amount, direction, fee and label when the wallet later only learns a new context for that transaction (an InstantSend lock, a block).
Current Behavior
After a restart, some transactions are held in the wallet as stand-in records that carry no accounting. When such a record's context changes, the event projection re-emits it as a full row, and the persisters write its empty values over the stored ones.
- The stand-ins. At load the FFI puts two kinds of record back with only the transaction and its context:
restore_unresolved_asset_lock_tx_records(the funding transaction of an asset lock still at Built / Broadcast, and the confirmed spenders of its inputs) andrestore_provider_special_txs. Both build aTransactionRecordwith no input details, no output details,net_amount = 0andTransactionDirection::Internal. - The projection re-emits them. The
BlockProcessedarm ofbuild_core_changesetre-emits everyupdatedrecord as a row. A stand-in keeps its txid resident in the account, so a block that confirms it updates the context of the stand-in and reports it as updated. Once fix(platform-wallet): store the InstantSend lock of a send on its record #5393 merges, theTransactionInstantLockedarm re-emits it too, one step earlier. - The persisters overwrite.
tx_record_to_ffiforwards the defaults, and Swift'supsertTransactionassignsdirection,netAmount,feeandlabelto the existing row. The Kotlin transaction callback replaces the same fields.
Result: the history row of an asset-lock funding transaction (or of a provider transaction) that was pending across a restart shows a zero amount and "internal" after its lock or block arrives.
This is from reading the code at the commits linked below; it has not been reproduced on a device.
Possible Solution
Root cause: the projection treats every record the wallet holds as a complete accounting snapshot, and a stand-in is not one.
- Stand-ins are built here:
restore_unresolved_asset_lock_tx_recordsandrestore_provider_special_txs. - Re-emitted here: the
BlockProcessedarm. - Overwritten here:
upsertTransaction.
Fix direction:
- Mark a stand-in where it is restored, so the projection can tell it from a record the wallet matched itself.
- Give the changeset a context-only update for such a record, and have every arm and every persister (SQLite, Swift, Kotlin) apply it without touching the stored accounting fields.
Two shortcuts that do not work:
- Recognising a stand-in by its shape (no details, zero net). A Standard or CoinJoin account matches a ProRegTx / ProUpRegTx / ProUpServTx by its payout script alone, and that record has the same shape while being complete.
- Leaving the stand-in out of the changeset. The FFI conversion drops the standalone lock map and takes a row's context from re-emitted records only (
from_changeset), so the stored context would stop advancing.
For a maintainer to decide: whether this is better solved by restoring complete records at load instead. #5220 replays recorded history on load for the FFI persister and touches the same restore path.
Steps to Reproduce (for bugs)
Not reproduced on a device; the sequence below follows from the code.
- Start an identity registration or top-up so that an asset lock is built and broadcast, and quit the app before the funding transaction is InstantSend-locked or mined.
- Relaunch. The funding transaction is restored as a stand-in on its funding account.
- Let the wallet see the block that confirms the funding transaction.
- Look at the stored transaction row: amount zero, direction internal, fee and label empty.
A unit-level check: restore a stand-in, apply a block (or, on #5393, a lock) for it, and assert on the records of the resulting CoreChangeSet and on the row a persister holds afterwards.
Context
Wrong amount and direction in the transaction history for a transaction that was pending across a restart; balances are not affected, since they come from the coin rows.
Found in the review of #5393 (#5393 (comment)), which adds the InstantSend arm's re-emission. Related: #5220, #5150, #5126.
Your Environment
- Platform
v5.0-devat2938e49dd18dafe9eb256b3cfdf22b2e16fdd7db - rust-dashcore pin
40268cc0402a8933ec539f16b2d634c4e25876ad - Hosts: Swift SDK (SwiftData persister), Kotlin SDK
Activity