Skip to content

platform-wallet: a transaction restored as a stand-in overwrites the stored row's amount and direction when its context changes #5416

Open

Description

@romchornyi

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.

  1. 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) and restore_provider_special_txs. Both build a TransactionRecord with no input details, no output details, net_amount = 0 and TransactionDirection::Internal.
  2. The projection re-emits them. The BlockProcessed arm of build_core_changeset re-emits every updated record 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, the TransactionInstantLocked arm re-emits it too, one step earlier.
  3. The persisters overwrite. tx_record_to_ffi forwards the defaults, and Swift's upsertTransaction assigns direction, netAmount, fee and label to 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.

Fix direction:

  1. Mark a stand-in where it is restored, so the projection can tell it from a record the wallet matched itself.
  2. 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.

  1. 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.
  2. Relaunch. The funding transaction is restored as a stand-in on its funding account.
  3. Let the wallet see the block that confirms the funding transaction.
  4. 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-dev at 2938e49dd18dafe9eb256b3cfdf22b2e16fdd7db
  • rust-dashcore pin 40268cc0402a8933ec539f16b2d634c4e25876ad
  • Hosts: Swift SDK (SwiftData persister), Kotlin SDK

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions