diff --git a/packages/rs-platform-wallet-ffi/src/manager.rs b/packages/rs-platform-wallet-ffi/src/manager.rs index 37661da3502..b875769a844 100644 --- a/packages/rs-platform-wallet-ffi/src/manager.rs +++ b/packages/rs-platform-wallet-ffi/src/manager.rs @@ -101,7 +101,7 @@ pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_seed( }; let option = PLATFORM_WALLET_MANAGER_STORAGE.with_item(manager_handle, |manager| { - runtime().block_on(manager.create_wallet_from_seed_bytes(network, seed, accounts)) + runtime().block_on(manager.create_wallet_from_seed_bytes(network, seed, accounts, None)) }); let result = unwrap_option_or_return!(option); let wallet = unwrap_result_or_return!(result); @@ -139,7 +139,12 @@ pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_mnemonic( }; let option = PLATFORM_WALLET_MANAGER_STORAGE.with_item(manager_handle, |manager| { - runtime().block_on(manager.create_wallet_from_mnemonic(mnemonic_str, network, accounts)) + runtime().block_on(manager.create_wallet_from_mnemonic( + mnemonic_str, + network, + accounts, + None, + )) }); let result = unwrap_option_or_return!(option); let wallet = unwrap_result_or_return!(result); diff --git a/packages/rs-platform-wallet/examples/basic_usage.rs b/packages/rs-platform-wallet/examples/basic_usage.rs index 20e6db8cd81..26913d5228a 100644 --- a/packages/rs-platform-wallet/examples/basic_usage.rs +++ b/packages/rs-platform-wallet/examples/basic_usage.rs @@ -60,6 +60,7 @@ async fn main() -> Result<(), Box> { Network::Testnet, seed_bytes, WalletAccountCreationOptions::Default, + None, ) .await?; diff --git a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs index 1042feb440a..ef44ebb28f1 100644 --- a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs +++ b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs @@ -56,11 +56,23 @@ impl PlatformWalletManager

{ /// [`parse_mnemonic_any_language`]). For passphrase-only flows or /// out-of-band seed material, derive the seed externally and use /// [`Self::create_wallet_from_seed_bytes`]. + /// + /// `birth_height_override` controls SPV's compact-filter scan + /// window for the new wallet. `None` (the default for fresh + /// wallets) seeds the birth height to SPV's current confirmed + /// header tip, so the scan window is `[H_now, ∞)` — anything + /// funded before init is invisible. `Some(0)` requests a full + /// historical scan from genesis (use sparingly — expensive on + /// long-lived chains, but required when an address may have + /// received funds before the wallet was first registered). + /// `Some(h)` pins the scan start to a specific block height, + /// useful when a known funding block is on record. pub async fn create_wallet_from_mnemonic( &self, mnemonic_phrase: &str, network: Network, accounts: WalletAccountCreationOptions, + birth_height_override: Option, ) -> Result, PlatformWalletError> { let mnemonic = parse_mnemonic_any_language(mnemonic_phrase) .map_err(|e| PlatformWalletError::WalletCreation(format!("Invalid mnemonic: {}", e)))?; @@ -70,16 +82,24 @@ impl PlatformWalletManager

{ e )) })?; - self.register_wallet(wallet).await + self.register_wallet(wallet, birth_height_override).await } /// Create a PlatformWallet from raw seed bytes, initialize persisted /// state, register it with the manager and return an `Arc` handle. + /// + /// See [`Self::create_wallet_from_mnemonic`] for the + /// `birth_height_override` semantics. `None` keeps the + /// pre-existing behaviour (scan from current SPV tip forward); + /// `Some(h)` is for callers that need to see funding deposited + /// before the wallet was registered (e.g. a long-lived bank + /// address pre-funded with testnet duffs). pub async fn create_wallet_from_seed_bytes( &self, network: Network, seed_bytes: [u8; 64], accounts: WalletAccountCreationOptions, + birth_height_override: Option, ) -> Result, PlatformWalletError> { let wallet = Wallet::from_seed_bytes(seed_bytes, network, accounts).map_err(|e| { PlatformWalletError::WalletCreation(format!( @@ -87,18 +107,39 @@ impl PlatformWalletManager

{ e )) })?; - self.register_wallet(wallet).await + self.register_wallet(wallet, birth_height_override).await } /// Register a pre-built `Wallet` with the manager: insert into the /// `WalletManager`, build a `PlatformWallet` handle, load persisted /// state, and return an `Arc` to the managed wallet. + /// + /// `birth_height_override` flows through to both the in-memory + /// `ManagedWalletInfo` sync checkpoint and the persisted + /// `WalletMetadataEntry` so the SPV scan window is consistent + /// across restarts. See [`Self::create_wallet_from_mnemonic`] for + /// the contract. #[allow(clippy::type_complexity)] async fn register_wallet( &self, wallet: Wallet, + birth_height_override: Option, ) -> Result, PlatformWalletError> { - let wallet_info = ManagedWalletInfo::from_wallet(&wallet, 0); + // Birth height resolution: explicit override wins; otherwise + // fall back to SPV's confirmed header tip (default for fresh + // wallets — they only need to see funding from now on); 0 if + // SPV isn't running yet. + let birth_height: u32 = match birth_height_override { + Some(h) => h, + None => self + .spv_manager + .sync_progress() + .await + .and_then(|p| p.headers().ok().map(|h| h.tip_height())) + .unwrap_or(0), + }; + + let wallet_info = ManagedWalletInfo::from_wallet(&wallet, birth_height); let balance = Arc::new(WalletBalance::new()); @@ -192,17 +233,10 @@ impl PlatformWalletManager

{ // the persister is a best-effort channel, not a source of // truth in steady state. - // Birth height = SPV's confirmed header tip if SPV is running, - // otherwise 0 (caller can bump it later when SPV catches up). - // 0 means "scan from genesis", which is safe-correct for - // fresh wallets. - let birth_height: u32 = self - .spv_manager - .sync_progress() - .await - .and_then(|p| p.headers().ok().map(|h| h.tip_height())) - .unwrap_or(0); - + // `birth_height` was resolved at the top of `register_wallet` + // and seeded into `ManagedWalletInfo`; reuse it here so the + // persisted `WalletMetadataEntry` agrees with the in-memory + // sync checkpoint. let mut registration_changeset = PlatformWalletChangeSet { wallet_metadata: Some(WalletMetadataEntry { network: self.sdk.network, diff --git a/packages/rs-platform-wallet/tests/spv_sync.rs b/packages/rs-platform-wallet/tests/spv_sync.rs index 86011d5ea84..fb621bafcd0 100644 --- a/packages/rs-platform-wallet/tests/spv_sync.rs +++ b/packages/rs-platform-wallet/tests/spv_sync.rs @@ -182,7 +182,12 @@ async fn test_spv_sync_and_balance() { let seed_bytes = mnemonic.to_seed(""); let platform_wallet = manager - .create_wallet_from_seed_bytes(network, seed_bytes, WalletAccountCreationOptions::Default) + .create_wallet_from_seed_bytes( + network, + seed_bytes, + WalletAccountCreationOptions::Default, + None, + ) .await .expect("Failed to create platform wallet");