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/5] 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/5] 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/5] 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"); From d0ebb7db2ab36066724cedfb04892f26384a02f7 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 19 May 2026 09:05:16 +0200 Subject: [PATCH 4/5] docs(rs-platform-wallet): correct birth_height_override None contract State the genesis fallback when SPV is offline so the public rustdoc matches register_wallet's unwrap_or(0) runtime behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/manager/wallet_lifecycle.rs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs index ef44ebb28f1..7517b1341d3 100644 --- a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs +++ b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs @@ -59,14 +59,17 @@ impl PlatformWalletManager

{ /// /// `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. + /// wallets) resolves the birth height from SPV's current + /// confirmed header tip, so the scan window is `[H_now, ∞)` and + /// anything funded before init is invisible — **but** when SPV is + /// not running yet or header state is unavailable (e.g. wallet + /// created before the SPV client is started), it falls back to + /// `0`, i.e. a full historical scan from genesis. `Some(0)` + /// always 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, @@ -89,11 +92,11 @@ impl PlatformWalletManager

{ /// 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). + /// `birth_height_override` semantics. `None` scans from the + /// current SPV tip forward when SPV is running, otherwise from + /// genesis; `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, From c8b8d732a3882acc4330704f9110fcac7847e3c8 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 19 May 2026 12:40:39 +0200 Subject: [PATCH 5/5] feat(rs-platform-wallet-ffi): expose birth_height_override via create_wallet_*_with_birth_height C exports Additive, non-breaking: two new `*_with_birth_height` C-ABI exports take a `has_birth_height_override: bool` + `birth_height_override: u32` pair mapped to `Option`; the existing seed/mnemonic exports stay byte-stable (still pass `None`). Swift `PlatformWalletManager` binding to the new symbols is deferred to the swift-rust-ffi-engineer specialist (out of scope here). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../rs-platform-wallet-ffi/src/manager.rs | 182 ++++++++++++++++-- 1 file changed, 170 insertions(+), 12 deletions(-) diff --git a/packages/rs-platform-wallet-ffi/src/manager.rs b/packages/rs-platform-wallet-ffi/src/manager.rs index b875769a844..322252166b4 100644 --- a/packages/rs-platform-wallet-ffi/src/manager.rs +++ b/packages/rs-platform-wallet-ffi/src/manager.rs @@ -65,17 +65,31 @@ pub unsafe extern "C" fn platform_wallet_manager_create( PlatformWalletFFIResult::ok() } -/// Create a wallet from raw seed bytes (64 bytes). +/// Map the C `has_x: bool` + `x` companion-pair idiom to a Rust `Option`. /// -/// On success, `out_wallet_handle` is set to a `PlatformWallet` handle and -/// `out_wallet_id` is filled with the 32-byte wallet ID. -#[no_mangle] -pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_seed( +/// `has == true` yields `Some(value)` — including `Some(0)`, kept distinct +/// from `has == false` which yields `None`. Mirrors the crate's `has_config` +/// optional-scalar convention; there is no `u32::MAX` sentinel. +fn birth_height_override_opt(has: bool, value: u32) -> Option { + if has { + Some(value) + } else { + None + } +} + +/// Shared body for the seed-based wallet-creation exports. +/// +/// `birth_height_override` is threaded verbatim into +/// `create_wallet_from_seed_bytes`; the no-override export passes `None`. +#[allow(clippy::too_many_arguments)] +unsafe fn create_wallet_from_seed_impl( manager_handle: Handle, network: FFINetwork, seed_bytes: *const u8, seed_len: usize, account_options: u32, + birth_height_override: Option, out_wallet_handle: *mut Handle, out_wallet_id: *mut [u8; 32], ) -> PlatformWalletFFIResult { @@ -101,7 +115,12 @@ 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, None)) + runtime().block_on(manager.create_wallet_from_seed_bytes( + network, + seed, + accounts, + birth_height_override, + )) }); let result = unwrap_option_or_return!(option); let wallet = unwrap_result_or_return!(result); @@ -112,16 +131,16 @@ pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_seed( PlatformWalletFFIResult::ok() } -/// Create a wallet from a BIP39 mnemonic phrase (English). +/// Shared body for the mnemonic-based wallet-creation exports. /// -/// On success, `out_wallet_handle` is set to a `PlatformWallet` handle and -/// `out_wallet_id` is filled with the 32-byte wallet ID. -#[no_mangle] -pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_mnemonic( +/// `birth_height_override` is threaded verbatim into +/// `create_wallet_from_mnemonic`; the no-override export passes `None`. +unsafe fn create_wallet_from_mnemonic_impl( manager_handle: Handle, mnemonic: *const std::os::raw::c_char, network: FFINetwork, account_options: u32, + birth_height_override: Option, out_wallet_handle: *mut Handle, out_wallet_id: *mut [u8; 32], ) -> PlatformWalletFFIResult { @@ -143,7 +162,7 @@ pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_mnemonic( mnemonic_str, network, accounts, - None, + birth_height_override, )) }); let result = unwrap_option_or_return!(option); @@ -155,6 +174,124 @@ pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_mnemonic( PlatformWalletFFIResult::ok() } +/// Create a wallet from raw seed bytes (64 bytes). +/// +/// On success, `out_wallet_handle` is set to a `PlatformWallet` handle and +/// `out_wallet_id` is filled with the 32-byte wallet ID. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_seed( + manager_handle: Handle, + network: FFINetwork, + seed_bytes: *const u8, + seed_len: usize, + account_options: u32, + out_wallet_handle: *mut Handle, + out_wallet_id: *mut [u8; 32], +) -> PlatformWalletFFIResult { + create_wallet_from_seed_impl( + manager_handle, + network, + seed_bytes, + seed_len, + account_options, + None, + out_wallet_handle, + out_wallet_id, + ) +} + +/// Create a wallet from raw seed bytes (64 bytes) with an optional +/// birth-height override. +/// +/// Identical to [`platform_wallet_manager_create_wallet_from_seed`] but lets +/// the caller pin the wallet's birth height. `has_birth_height_override == +/// false` behaves exactly like the no-override export (`None`); +/// `has_birth_height_override == true` passes `Some(birth_height_override)`, +/// including `Some(0)`. +/// +/// On success, `out_wallet_handle` is set to a `PlatformWallet` handle and +/// `out_wallet_id` is filled with the 32-byte wallet ID. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_seed_with_birth_height( + manager_handle: Handle, + network: FFINetwork, + seed_bytes: *const u8, + seed_len: usize, + account_options: u32, + has_birth_height_override: bool, + birth_height_override: u32, + out_wallet_handle: *mut Handle, + out_wallet_id: *mut [u8; 32], +) -> PlatformWalletFFIResult { + create_wallet_from_seed_impl( + manager_handle, + network, + seed_bytes, + seed_len, + account_options, + birth_height_override_opt(has_birth_height_override, birth_height_override), + out_wallet_handle, + out_wallet_id, + ) +} + +/// Create a wallet from a BIP39 mnemonic phrase (English). +/// +/// On success, `out_wallet_handle` is set to a `PlatformWallet` handle and +/// `out_wallet_id` is filled with the 32-byte wallet ID. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_mnemonic( + manager_handle: Handle, + mnemonic: *const std::os::raw::c_char, + network: FFINetwork, + account_options: u32, + out_wallet_handle: *mut Handle, + out_wallet_id: *mut [u8; 32], +) -> PlatformWalletFFIResult { + create_wallet_from_mnemonic_impl( + manager_handle, + mnemonic, + network, + account_options, + None, + out_wallet_handle, + out_wallet_id, + ) +} + +/// Create a wallet from a BIP39 mnemonic phrase (English) with an optional +/// birth-height override. +/// +/// Identical to [`platform_wallet_manager_create_wallet_from_mnemonic`] but +/// lets the caller pin the wallet's birth height. `has_birth_height_override +/// == false` behaves exactly like the no-override export (`None`); +/// `has_birth_height_override == true` passes `Some(birth_height_override)`, +/// including `Some(0)`. +/// +/// On success, `out_wallet_handle` is set to a `PlatformWallet` handle and +/// `out_wallet_id` is filled with the 32-byte wallet ID. +#[no_mangle] +pub unsafe extern "C" fn platform_wallet_manager_create_wallet_from_mnemonic_with_birth_height( + manager_handle: Handle, + mnemonic: *const std::os::raw::c_char, + network: FFINetwork, + account_options: u32, + has_birth_height_override: bool, + birth_height_override: u32, + out_wallet_handle: *mut Handle, + out_wallet_id: *mut [u8; 32], +) -> PlatformWalletFFIResult { + create_wallet_from_mnemonic_impl( + manager_handle, + mnemonic, + network, + account_options, + birth_height_override_opt(has_birth_height_override, birth_height_override), + out_wallet_handle, + out_wallet_id, + ) +} + /// Hydrate the manager from its persister. /// /// Triggers `on_load_wallet_list_fn` on the persistence callbacks to @@ -219,3 +356,24 @@ pub unsafe extern "C" fn platform_wallet_manager_destroy( } PlatformWalletFFIResult::ok() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn birth_height_override_opt_true_zero_is_some_zero() { + assert_eq!(birth_height_override_opt(true, 0), Some(0)); + } + + #[test] + fn birth_height_override_opt_true_value_is_some_value() { + assert_eq!(birth_height_override_opt(true, 42), Some(42)); + } + + #[test] + fn birth_height_override_opt_false_is_none_regardless_of_value() { + assert_eq!(birth_height_override_opt(false, 0), None); + assert_eq!(birth_height_override_opt(false, 99), None); + } +}