Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
90 changes: 67 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,20 @@ impl Database {
Ok(wallets_map.into_values().collect())
}

/// Store or update Platform address balance and nonce
/// Store or update Platform address balance and nonce.
///
/// When `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) and false for
/// internal updates (e.g., after a transfer completes), so that subsequent terminal syncs
/// can correctly apply any pending AddToCredits.
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 +911,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 +1272,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 +1285,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 +1383,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 +1421,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
Loading
Loading