Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/backend_task/wallet/fetch_platform_address_balances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,20 +196,21 @@ impl AppContext {
let wallet = wallet_arc.read().map_err(|e| e.to_string())?;
for (core_addr, platform_addr) in wallet.platform_addresses(self.network) {
if let Some(info) = wallet.get_platform_address_info(&core_addr) {
// Only pre-populate if we have a last_synced_balance
// Only pre-populate if we have a last_full_sync_balance
// (meaning this address was found in a previous full sync)
if let Some(synced_balance) = info.last_synced_balance {
// This prevents double-counting AddToCredits after app restart
if let Some(full_sync_balance) = info.last_full_sync_balance {
let lookup_addr = platform_addr.to_address_with_network(self.network);
provider.update_balance(&lookup_addr, synced_balance);
provider.update_balance(&lookup_addr, full_sync_balance);
pre_populated_count += 1;
tracing::debug!(
"Pre-populated balance for {}: {} (last synced)",
"Pre-populated balance for {}: {} (from last full sync)",
platform_addr.to_bech32m_string(self.network),
synced_balance
full_sync_balance
);
} else {
tracing::debug!(
"Skipping pre-population for {} (no last_synced_balance, likely from proof)",
"Skipping pre-population for {} (no last_full_sync_balance, needs full sync)",
platform_addr.to_bech32m_string(self.network)
);
}
Expand Down Expand Up @@ -259,6 +260,7 @@ impl AppContext {
let balances = {
let mut wallet = wallet_arc.write().map_err(|e| e.to_string())?;

// Update wallet with synced balances (also updates last_full_sync_balance for next sync)
provider.apply_results_to_wallet(&mut wallet);

// Persist addresses and balances to database
Expand All @@ -284,12 +286,14 @@ impl AppContext {

// Persist balance to platform_address_balances table
// Use the nonce from AddressFunds which comes directly from SDK sync
// This is a sync operation, so update last_full_sync_balance
if let Err(e) = self.db.set_platform_address_info(
&seed_hash,
address,
funds.balance,
funds.nonce,
&self.network,
true, // Sync operation - update last_full_sync_balance
) {
tracing::warn!("Failed to persist Platform address info: {}", e);
}
Expand Down
4 changes: 3 additions & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,15 @@ impl AppContext {
// Update in-memory wallet state
wallet.set_platform_address_info(core_addr.clone(), info.balance, info.nonce);

// Update database
// Update database (not a sync operation - preserve last_full_sync_balance
// so the next terminal sync can correctly apply any pending AddToCredits)
if let Err(e) = self.db.set_platform_address_info(
&seed_hash,
&core_addr,
info.balance,
info.nonce,
&self.network,
false, // Not a sync operation
) {
tracing::warn!("Failed to store Platform address info in database: {}", e);
}
Expand Down
40 changes: 39 additions & 1 deletion src/database/initialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use rusqlite::{Connection, params};
use std::fs;
use std::path::Path;

pub const DEFAULT_DB_VERSION: u16 = 25;
pub const DEFAULT_DB_VERSION: u16 = 26;

pub const DEFAULT_NETWORK: &str = "dash";

Expand Down Expand Up @@ -51,6 +51,9 @@ impl Database {

fn apply_version_changes(&self, version: u16, tx: &Connection) -> rusqlite::Result<()> {
match version {
26 => {
self.add_last_full_sync_balance_column(tx)?;
}
25 => {
self.add_avatar_bytes_column(tx)?;
}
Expand Down Expand Up @@ -342,6 +345,7 @@ impl Database {
nonce INTEGER NOT NULL DEFAULT 0,
network TEXT NOT NULL,
updated_at INTEGER NOT NULL DEFAULT 0,
last_full_sync_balance INTEGER DEFAULT NULL,
PRIMARY KEY (seed_hash, address, network),
FOREIGN KEY (seed_hash) REFERENCES wallet(seed_hash) ON DELETE CASCADE
)",
Expand Down Expand Up @@ -784,6 +788,40 @@ impl Database {

Ok(())
}

/// Migration: Add last_full_sync_balance column to platform_address_balances table (version 26).
/// Stores the balance from the last FULL sync (checkpoint), separate from the current balance
/// which includes terminal sync updates. This prevents double-counting AddToCredits during
/// terminal-only syncs after app restart.
fn add_last_full_sync_balance_column(&self, conn: &Connection) -> rusqlite::Result<()> {
// Check if platform_address_balances table exists
let table_exists: bool = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='platform_address_balances'",
[],
|row| row.get::<_, i32>(0).map(|count| count > 0),
)?;

if table_exists {
// Check if last_full_sync_balance column already exists
let has_column: bool = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('platform_address_balances') WHERE name='last_full_sync_balance'",
[],
|row| row.get::<_, i32>(0).map(|count| count > 0),
)
.unwrap_or(false);

if !has_column {
// Add column with NULL default - existing rows will need a full sync to populate
conn.execute(
"ALTER TABLE platform_address_balances ADD COLUMN last_full_sync_balance INTEGER DEFAULT NULL",
[],
)?;
}
}

Ok(())
}
}

#[cfg(test)]
Expand Down
93 changes: 70 additions & 23 deletions src/database/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,20 +837,27 @@ impl Database {
);
// Load platform address info for each wallet (using existing connection to avoid deadlock)
let mut platform_stmt = conn.prepare(
"SELECT seed_hash, address, balance, nonce FROM platform_address_balances WHERE network = ?",
"SELECT seed_hash, address, balance, nonce, last_full_sync_balance FROM platform_address_balances WHERE network = ?",
)?;
let platform_rows = platform_stmt.query_map([network_str.clone()], |row| {
let seed_hash: Vec<u8> = row.get(0)?;
let address_str: String = row.get(1)?;
let balance: i64 = row.get(2)?;
let nonce: i64 = row.get(3)?;
let last_full_sync_balance: Option<i64> = row.get(4)?;
let seed_hash_array: [u8; 32] =
seed_hash.try_into().expect("Seed hash should be 32 bytes");
Ok((seed_hash_array, address_str, balance as u64, nonce as u32))
Ok((
seed_hash_array,
address_str,
balance as u64,
nonce as u32,
last_full_sync_balance.map(|b| b as u64),
))
})?;

for row in platform_rows {
if let Ok((seed_hash, address_str, balance, nonce)) = row
if let Ok((seed_hash, address_str, balance, nonce, last_full_sync_balance)) = row
&& let Some(wallet) = wallets_map.get_mut(&seed_hash)
&& let Ok(address) = Address::<NetworkUnchecked>::from_str(&address_str)
{
Expand All @@ -869,8 +876,9 @@ impl Database {
crate::model::wallet::PlatformAddressInfo {
balance,
nonce,
// Assume database balance is from sync (safe default)
last_synced_balance: Some(balance),
// Use the stored last_full_sync_balance from the database
// This is the balance from the last FULL sync checkpoint, not including terminal updates
last_full_sync_balance,
},
);
}
Expand All @@ -880,14 +888,23 @@ impl Database {
Ok(wallets_map.into_values().collect())
}

/// Store or update Platform address balance and nonce
/// Store or update Platform address balance and nonce.
/// If `is_full_sync` is true, also updates `last_full_sync_balance` to the current balance.
/// This should only be set to true during full syncs (not terminal-only syncs).
/// Store or update Platform address balance and nonce.
///
/// If `is_sync_operation` is true, also updates `last_full_sync_balance` to the current balance.
/// This should be true for sync operations (full or terminal) but false for internal updates
/// (like after a transfer completes), so that the next terminal sync can correctly apply
/// any pending AddToCredits.
Comment thread
pauldelucia marked this conversation as resolved.
Outdated
pub fn set_platform_address_info(
&self,
seed_hash: &[u8; 32],
address: &Address,
balance: u64,
nonce: u32,
network: &Network,
is_sync_operation: bool,
) -> rusqlite::Result<()> {
let network_str = network.to_string();
let canonical_address = Wallet::canonical_address(address, *network);
Expand All @@ -897,19 +914,49 @@ impl Database {
.unwrap_or_default()
.as_secs() as i64;

self.execute(
"INSERT OR REPLACE INTO platform_address_balances
(seed_hash, address, balance, nonce, network, updated_at)
VALUES (?, ?, ?, ?, ?, ?)",
params![
seed_hash,
address_str,
balance as i64,
nonce as i64,
network_str,
updated_at
],
)?;
if is_sync_operation {
// Sync operation: update both balance and last_full_sync_balance
// last_full_sync_balance becomes the baseline for pre-population in the next sync
self.execute(
"INSERT INTO platform_address_balances
(seed_hash, address, balance, nonce, network, updated_at, last_full_sync_balance)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(seed_hash, address, network) DO UPDATE SET
balance = excluded.balance,
nonce = excluded.nonce,
updated_at = excluded.updated_at,
last_full_sync_balance = excluded.last_full_sync_balance",
params![
seed_hash,
address_str,
balance as i64,
nonce as i64,
network_str,
updated_at,
balance as i64
],
)?;
} else {
// Internal update (e.g., after transfer): update balance but preserve last_full_sync_balance
// This ensures the next terminal sync correctly applies any pending AddToCredits
self.execute(
"INSERT INTO platform_address_balances
(seed_hash, address, balance, nonce, network, updated_at, last_full_sync_balance)
VALUES (?, ?, ?, ?, ?, ?, NULL)
ON CONFLICT(seed_hash, address, network) DO UPDATE SET
balance = excluded.balance,
nonce = excluded.nonce,
updated_at = excluded.updated_at",
params![
seed_hash,
address_str,
balance as i64,
nonce as i64,
network_str,
updated_at
],
)?;
}
Ok(())
}

Expand Down Expand Up @@ -1228,7 +1275,7 @@ mod tests {
assert!(info.is_none());

// Set platform address info
db.set_platform_address_info(&seed_hash, &address, 10_000_000, 5, &network)
db.set_platform_address_info(&seed_hash, &address, 10_000_000, 5, &network, true)
.expect("Failed to set platform address info");

// Retrieve it
Expand All @@ -1241,7 +1288,7 @@ mod tests {
assert_eq!(info.1, 5); // nonce

// Update it
db.set_platform_address_info(&seed_hash, &address, 20_000_000, 10, &network)
db.set_platform_address_info(&seed_hash, &address, 20_000_000, 10, &network, true)
.expect("Failed to update platform address info");

let info = db
Expand Down Expand Up @@ -1339,7 +1386,7 @@ mod tests {

// Add a single valid platform address using the helper function
let address = create_test_address(network);
db.set_platform_address_info(&seed_hash, &address, 5_000_000, 3, &network)
db.set_platform_address_info(&seed_hash, &address, 5_000_000, 3, &network, true)
.expect("Failed to set platform address info");

// Get all addresses
Expand Down Expand Up @@ -1377,7 +1424,7 @@ mod tests {
}

// Set platform address info
db.set_platform_address_info(&seed_hash, &address, 10_000_000, 5, &network)
db.set_platform_address_info(&seed_hash, &address, 10_000_000, 5, &network, true)
.expect("Failed to set platform address info");

// Verify it exists
Expand Down
24 changes: 14 additions & 10 deletions src/model/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,10 @@ impl PartialEq for WalletArcRef {
pub struct PlatformAddressInfo {
pub balance: Credits,
pub nonce: AddressNonce,
/// Balance as of last full sync (used for terminal-only sync pre-population)
/// This prevents double-counting when proof-verified updates happen between syncs
pub last_synced_balance: Option<Credits>,
/// Balance as of last FULL sync checkpoint (not including terminal updates).
/// Used for terminal-only sync pre-population to prevent double-counting AddToCredits.
/// Only set during full syncs, preserved during terminal syncs.
pub last_full_sync_balance: Option<Credits>,
}
Comment thread
pauldelucia marked this conversation as resolved.
Outdated

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -1916,23 +1917,25 @@ impl Wallet {
}
}

// Preserve last_synced_balance if it exists
let last_synced_balance = self
// Preserve last_full_sync_balance if it exists
let last_full_sync_balance = self
.platform_address_info
.get(&address)
.and_then(|info| info.last_synced_balance);
.and_then(|info| info.last_full_sync_balance);

self.platform_address_info.insert(
address,
PlatformAddressInfo {
balance,
nonce,
last_synced_balance,
last_full_sync_balance,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
);
}

/// Set platform address info from a sync operation (updates last_synced_balance)
/// Set platform address info from a sync operation.
/// Always updates `last_full_sync_balance` to the current balance, as this becomes
/// the baseline for pre-population in the next terminal sync.
pub fn set_platform_address_info_from_sync(
&mut self,
address: Address,
Expand All @@ -1944,7 +1947,8 @@ impl Wallet {
PlatformAddressInfo {
balance,
nonce,
last_synced_balance: Some(balance),
// Always update to current balance - this is the baseline for next sync
last_full_sync_balance: Some(balance),
},
);
}
Expand Down Expand Up @@ -2216,7 +2220,7 @@ impl WalletAddressProvider {
for (address, funds) in &self.found_balances {
let canonical_address = Wallet::canonical_address(address, self.network);

// Use sync-specific method that also updates last_synced_balance
// Update wallet with synced balance (also updates last_full_sync_balance for next sync)
wallet.set_platform_address_info_from_sync(
canonical_address.clone(),
funds.balance,
Expand Down
Loading