From b6e9dcb7bcff08cbe7632eeda1dc204458cae542 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 5 May 2026 15:09:23 +0200 Subject: [PATCH 1/3] feat(rs-platform-wallet): add birth_height_override to wallet creation API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both `create_wallet_from_mnemonic` and `create_wallet_from_seed_bytes` now take a `birth_height_override: Option` controlling the SPV compact-filter scan window for the new wallet: - `None` keeps the prior behaviour (seed birth height to SPV's current confirmed header tip — fine for fresh wallets that only need to see funding from now on). - `Some(0)` requests a full historical scan from genesis, required when an address may have received funds before registration. - `Some(h)` pins the scan to a specific block height. The override flows through `register_wallet` into both the in-memory `ManagedWalletInfo` checkpoint and the persisted `WalletMetadataEntry` so the SPV scan window is consistent across restarts. Previously those two carried independent values (in-memory hardcoded to 0, persisted seeded from SPV tip), which was incoherent. FFI bindings and the basic_usage example pass `None` to preserve existing semantics. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../rs-platform-wallet-ffi/src/manager.rs | 9 ++- .../examples/basic_usage.rs | 1 + .../src/manager/wallet_lifecycle.rs | 62 ++++++++++++++----- 3 files changed, 56 insertions(+), 16 deletions(-) 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, From 3cf159fa30d669b7aa9f367964a4e105f9db76b8 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 13 May 2026 12:19:47 +0200 Subject: [PATCH 2/3] test(rs-platform-wallet): update spv_sync caller for birth_height_override The new birth_height_override parameter introduced in the previous commit requires existing call-sites to be updated. The integration test `spv_sync` now passes `None` to preserve prior behaviour (birth-seed from SPV header tip). Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-platform-wallet/tests/spv_sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-platform-wallet/tests/spv_sync.rs b/packages/rs-platform-wallet/tests/spv_sync.rs index 86011d5ea84..c2bf0ffb46d 100644 --- a/packages/rs-platform-wallet/tests/spv_sync.rs +++ b/packages/rs-platform-wallet/tests/spv_sync.rs @@ -182,7 +182,7 @@ 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"); From 59d49f95f1439cb7ad71a3abc2cdf247563fb7c9 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 13 May 2026 12:20:15 +0200 Subject: [PATCH 3/3] style(rs-platform-wallet): rustfmt spv_sync caller Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/rs-platform-wallet/tests/spv_sync.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/rs-platform-wallet/tests/spv_sync.rs b/packages/rs-platform-wallet/tests/spv_sync.rs index c2bf0ffb46d..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, None) + .create_wallet_from_seed_bytes( + network, + seed_bytes, + WalletAccountCreationOptions::Default, + None, + ) .await .expect("Failed to create platform wallet");