Skip to content
Merged
187 changes: 175 additions & 12 deletions packages/rs-platform-wallet-ffi/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>`.
///
/// 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<u32> {
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<u32>,
out_wallet_handle: *mut Handle,
out_wallet_id: *mut [u8; 32],
) -> PlatformWalletFFIResult {
Expand All @@ -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);
Expand All @@ -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<u32>,
out_wallet_handle: *mut Handle,
out_wallet_id: *mut [u8; 32],
) -> PlatformWalletFFIResult {
Expand All @@ -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,
))
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.
Comment thread
lklimek marked this conversation as resolved.
});
let result = unwrap_option_or_return!(option);
let wallet = unwrap_result_or_return!(result);
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions packages/rs-platform-wallet/examples/basic_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Network::Testnet,
seed_bytes,
WalletAccountCreationOptions::Default,
None,
)
.await?;

Expand Down
65 changes: 51 additions & 14 deletions packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,26 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
/// [`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<u32>,
) -> Result<Arc<PlatformWallet>, PlatformWalletError> {
let mnemonic = parse_mnemonic_any_language(mnemonic_phrase)
.map_err(|e| PlatformWalletError::WalletCreation(format!("Invalid mnemonic: {}", e)))?;
Expand All @@ -70,35 +85,64 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
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<u32>,
) -> Result<Arc<PlatformWallet>, PlatformWalletError> {
let wallet = Wallet::from_seed_bytes(seed_bytes, network, accounts).map_err(|e| {
PlatformWalletError::WalletCreation(format!(
"Failed to create wallet from seed bytes: {}",
e
))
})?;
self.register_wallet(wallet).await
self.register_wallet(wallet, birth_height_override).await
}
Comment thread
lklimek marked this conversation as resolved.

/// 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<u32>,
) -> Result<Arc<PlatformWallet>, 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),
};
Comment on lines 126 to +143
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Option<u32> overloads three distinct birth-height policies

birth_height_override: Option<u32> encodes three semantically different policies — 'pick a default from SPV state (else fall back to 0)', 'force a full genesis rescan' (Some(0)), and 'pin to height h' (Some(h>0)). The rustdoc at lines 60-72 spells this out because the type itself doesn't, and Some(0) colliding with the None-fallback value is a foot-gun. A dedicated enum (e.g. BirthHeightPolicy::{Auto, FromGenesis, At(u32)}) would make the three intents explicit at every call site and let the resolver match exhaustively. Not blocking; easiest to reshape now while the API has no external Rust consumers, harder once iOS picks it up.

source: ['claude']


let wallet_info = ManagedWalletInfo::from_wallet(&wallet, birth_height);

let balance = Arc::new(WalletBalance::new());

Expand Down Expand Up @@ -192,17 +236,10 @@ impl<P: PlatformWalletPersistence + 'static> PlatformWalletManager<P> {
// 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,
Expand Down
7 changes: 6 additions & 1 deletion packages/rs-platform-wallet/tests/spv_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Loading