diff --git a/packages/rs-platform-wallet-ffi/src/manager.rs b/packages/rs-platform-wallet-ffi/src/manager.rs index aa00c0831c6..0f46906fcd1 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)) + 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 { @@ -139,7 +158,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, + birth_height_override, + )) }); let result = unwrap_option_or_return!(option); let wallet = unwrap_result_or_return!(result); @@ -150,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 @@ -245,3 +387,24 @@ pub unsafe extern "C" fn platform_wallet_manager_remove_wallet( ), } } + +#[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); + } +} 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 59649f672c9..5f16853202d 100644 --- a/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs +++ b/packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs @@ -56,11 +56,26 @@ 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) 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, 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 +85,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` 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, 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 +110,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 +236,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");