Skip to content
Open
483 changes: 477 additions & 6 deletions packages/rs-platform-wallet-ffi/src/persistence.rs

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,42 @@ pub struct UnconfirmedOutgoingTxRecordFFI {
pub first_seen: u64,
}

/// One stored transaction of the wallet, handed back at load so the shared
/// history replay can rebuild the spend guards of confirmed AND unconfirmed
/// spends.
///
/// Without it only the persisted unspent UTXO set comes back, so a funding
/// transaction redelivered after restart (rescan, gap-limit rediscovery)
/// re-credits an output a confirmed spend already consumed.
#[repr(C)]
pub struct RecordedTransactionRestoreFFI {
/// Wire-order txid of the stored row. The load path decodes `tx_bytes`
/// and drops the record unless it hashes to this: the replay applies the
/// transaction through the ordinary state-update path, so foreign bytes
/// would move accounting for unrelated inputs and outputs.
pub txid: [u8; 32],
/// Consensus-encoded transaction body. Swift-owned for the callback
/// window; freed by `LoadWalletListFreeFn`.
pub tx_bytes: *mut u8,
pub tx_bytes_len: usize,
/// Stored context: `0` mempool, `1` InstantSend, `2` in a block, `3` in a
/// chain-locked block. `1` replays as mempool (no lock bytes are carried);
/// any other value drops the record.
pub context: u32,
/// Block fields, read only for contexts `2` and `3`.
pub block_height: u32,
pub block_hash: [u8; 32],
pub block_timestamp: u32,
/// In-block position, meaningful only when `has_block_position`.
pub block_position: u32,
pub has_block_position: bool,
/// The wallet-level net amount the host stores for this row.
pub net_amount: i64,
/// The stored direction: `0` incoming, `1` outgoing, `2` internal,
/// `3` CoinJoin. Any other value reads as "unknown".
pub direction: u32,
}

/// Per-wallet entry returned by `on_load_wallet_list_fn`.
///
/// `accounts` points to a contiguous array of length `accounts_count`.
Expand Down Expand Up @@ -718,6 +754,16 @@ pub struct WalletRestoreEntryFFI {
/// leaves every existing field where it was.
pub unconfirmed_outgoing_tx_records: *const UnconfirmedOutgoingTxRecordFFI,
pub unconfirmed_outgoing_tx_records_count: usize,
/// Every stored transaction of this wallet, in any order — see
/// [`RecordedTransactionRestoreFFI`]. `null` / `0` means "no history
/// supplied" and keeps the pre-history load behaviour. Each entry's
/// `tx_bytes` buffer is Swift-owned and freed by `LoadWalletListFreeFn`.
///
/// Appended at the end for the same reason as the field above; the
/// struct carries no size field, so host and library must still be
/// built together.
pub recorded_transactions: *const RecordedTransactionRestoreFFI,
pub recorded_transactions_count: usize,
}

/// Every field named explicitly so that adding a field to this ABI struct
Expand Down Expand Up @@ -758,6 +804,8 @@ impl Default for WalletRestoreEntryFFI {
last_applied_chain_lock_bytes_len: 0,
unconfirmed_outgoing_tx_records: std::ptr::null(),
unconfirmed_outgoing_tx_records_count: 0,
recorded_transactions: std::ptr::null(),
recorded_transactions_count: 0,
}
}
}
Expand Down
24 changes: 15 additions & 9 deletions packages/rs-platform-wallet-storage/src/sqlite/persister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::sqlite::error::{AutoBackupOperation, WalletStorageError};
use crate::sqlite::load_ctx::{LoadCtx, LoadDegradation, LoadSite};
use crate::sqlite::rehydrate::{
apply_persisted_core_state, build_wallet, restore_provider_platform_node_pool,
restore_recorded_transactions,
};
use crate::sqlite::reports::{CommitReport, DeleteWalletReport};
use crate::sqlite::schema;
Expand Down Expand Up @@ -1322,7 +1321,9 @@ impl PlatformWalletPersistence for SqlitePersister {
.union(PersistenceCapabilities::TRACKED_ASSET_LOCKS)
.union(PersistenceCapabilities::TRACKED_MASTERNODES)
.union(PersistenceCapabilities::CORE_SWEEP_REMOVAL)
.union(PersistenceCapabilities::DASHPAY_PAYMENTS);
.union(PersistenceCapabilities::DASHPAY_PAYMENTS)
// `load` hands every stored record back as `recorded_history`.
.union(PersistenceCapabilities::CORE_HISTORY_RESTORE);
#[cfg(feature = "shielded")]
{
capabilities.union(PersistenceCapabilities::SHIELDED_VIEWING_KEYS)
Expand Down Expand Up @@ -1850,13 +1851,17 @@ fn load_one_wallet(
))
})?;
}
let mut wallet = wallet;
restore_recorded_transactions(
&mut wallet_info,
&mut wallet,
core_state.records,
&core_state.instant_locks_for_non_final_records,
);
// The stored records are replayed by the shared load
// (`platform_wallet::manager::history_replay`), which rebuilds the spend
// guards on top of the projection restored above.
let recorded_history = platform_wallet::changeset::RecordedHistory {
transactions: core_state
.records
.into_iter()
.map(platform_wallet::changeset::StoredTransaction::from)
.collect(),
instant_locks: core_state.instant_locks_for_non_final_records,
};
Ok(platform_wallet::changeset::ClientWalletStartState {
wallet,
wallet_info,
Expand All @@ -1867,6 +1872,7 @@ fn load_one_wallet(
// replay inert here, which is the behaviour this path had before the
// field existed.
unconfirmed_outgoing_txs: Vec::new(),
recorded_history,
})
}

Expand Down
Loading
Loading