Skip to content

feat(platform-wallet): add birth_height_override to wallet creation API#3636

Open
lklimek wants to merge 4 commits into
v3.1-devfrom
feat/rs-platform-wallet-birth-height-override
Open

feat(platform-wallet): add birth_height_override to wallet creation API#3636
lklimek wants to merge 4 commits into
v3.1-devfrom
feat/rs-platform-wallet-birth-height-override

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented May 13, 2026

Issue being fixed or feature implemented

Splits a self-contained production-code change out of #3549 (the large e2e framework PR) so it can be reviewed and merged on its own cadence.

The previous wallet-creation flow stored two independent birth heights for a new wallet:

  • The in-memory ManagedWalletInfo checkpoint was hardcoded to 0.
  • The persisted WalletMetadataEntry was seeded from the SPV header tip.

Those two could disagree after a restart, which made SPV compact-filter scans inconsistent across process lifecycles and could cause the scan window to miss blocks containing existing wallet funding.

What was done?

  • Add birth_height_override: Option<u32> to create_wallet_from_mnemonic and create_wallet_from_seed_bytes in manager/wallet_lifecycle.rs.
  • Resolution: Some(h) => pin the scan window to height h (use Some(0) for a full historical scan from genesis); None => seed from SPV header tip (prior behaviour).
  • Plumb the resolved height through register_wallet into both the in-memory ManagedWalletInfo and the persisted WalletMetadataEntry, so the two stay in lock-step.
  • Update FFI bindings (platform-wallet-ffi/src/manager.rs) and the basic_usage example to pass None — preserves existing semantics on those surfaces.
  • Update the spv_sync integration test caller (and apply rustfmt) to satisfy the new signature.

How Has This Been Tested?

  • cargo check -p platform-wallet --tests --all-features — clean.
  • cargo clippy -p platform-wallet -p platform-wallet-ffi --no-deps -- -D warnings — clean.
  • cargo fmt clean on all files touched by this PR.
  • Exercised end-to-end on test(platform-wallet): e2e framework + 60-test suite, 5 upstream bug pins #3549's e2e framework branch (bank-wallet passes Some(0) to force genesis rescan; regular test wallets pass None).

Note: there is a pre-existing compile failure in packages/rs-platform-wallet-ffi/tests/integration_tests.rs on v3.1-dev that calls platform_wallet_info_create_from_mnemonic with 4 args against a 3-arg signature. It is independent of this PR and reproduces on plain v3.1-dev.

Breaking Changes

None on public FFI surface — the FFI helpers (platform-wallet-ffi/src/manager.rs) absorb the new parameter and continue to expose the same C ABI. Direct Rust callers of create_wallet_from_mnemonic / create_wallet_from_seed_bytes need to add an Option<u32> argument; passing None preserves the previous behaviour.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

Release Notes

  • New Features
    • Added optional birth height override parameter to wallet creation functions, allowing explicit control over SPV scan initialization timing.

Review Change Stack

lklimek and others added 3 commits May 13, 2026 12:15
…n API

Both `create_wallet_from_mnemonic` and `create_wallet_from_seed_bytes`
now take a `birth_height_override: Option<u32>` 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) <noreply@anthropic.com>
…rride

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) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

The PR extends wallet creation methods with an optional birth_height_override parameter to control SPV scan birth height at registration time. The core logic resolves an effective birth height from the override, SPV sync progress, or defaults to zero, then uses it consistently in both in-memory and persisted wallet state. Call sites (FFI, example, test) are updated to pass None.

Changes

Birth Height Override Feature

Layer / File(s) Summary
Wallet creation API and birth height resolution
packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
create_wallet_from_mnemonic and create_wallet_from_seed_bytes now accept birth_height_override: Option<u32>. The internal register_wallet resolves the effective birth height from the override (preferred), SPV confirmed header tip, or defaults to 0, then uses it to construct both ManagedWalletInfo and persisted WalletMetadataEntry.
Call site updates
packages/rs-platform-wallet-ffi/src/manager.rs, packages/rs-platform-wallet/examples/basic_usage.rs, packages/rs-platform-wallet/tests/spv_sync.rs
FFI wrapper methods, example usage, and integration tests are updated to pass None for the new birth_height_override parameter in wallet creation calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 A birth height blooms in wallet creation's garden,
Where SPV tips sway and overrides pardon,
Through mnemonic and seed the parameter flows,
Metadata and memory in harmony, it grows!
✨🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(platform-wallet): add birth_height_override to wallet creation API' directly and concisely describes the main change: adding an optional birth_height_override parameter to wallet creation functions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rs-platform-wallet-birth-height-override

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added this to the v3.1.0 milestone May 13, 2026
@lklimek lklimek changed the title feat(rs-platform-wallet): add birth_height_override to wallet creation API feat(platform-wallet): add birth_height_override to wallet creation API May 13, 2026
@lklimek lklimek marked this pull request as ready for review May 13, 2026 11:09
@lklimek lklimek requested a review from QuantumExplorer as a code owner May 13, 2026 11:09
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented May 13, 2026

✅ Review complete (commit e246dd3)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- Around line 60-69: The comment for birth_height_override is inaccurate: None
currently only resolves to SPV's confirmed tip when sync_progress() already has
headers, otherwise the code falls back to genesis (unwrap_or(0)); update
behavior so the public contract matches runtime behavior by either (a) changing
the comment on birth_height_override to state the real fallback (None -> use
sync_progress() tip if available, otherwise fall back to genesis), or (b) change
the implementation that resolves None to consult a durable/persisted header tip
from the SPV subsystem (e.g., call the persisted tip API instead of relying
solely on sync_progress()), then use that tip height as the default; update all
places referencing birth_height_override and sync_progress() accordingly (also
fix similar wording/behavior where birth_height_override is documented at the
other occurrences).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80ec229c-b1db-4ad9-be12-28b41bae6085

📥 Commits

Reviewing files that changed from the base of the PR and between fa1e492 and 59d49f9.

📒 Files selected for processing (4)
  • packages/rs-platform-wallet-ffi/src/manager.rs
  • packages/rs-platform-wallet/examples/basic_usage.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/rs-platform-wallet/tests/spv_sync.rs

Comment on lines +60 to +69
/// `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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the None contract with the actual fallback.

None only resolves to the current confirmed tip when sync_progress() already has headers. If the wallet is created before SPV starts or before headers are available—which is exactly what the updated example and spv_sync test do—the unwrap_or(0) path still seeds genesis and triggers a historical scan. Please either document that fallback here or resolve None from a durable header tip so the public contract matches runtime behavior.

Also applies to: 91-96, 128-140

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs` around lines 60
- 69, The comment for birth_height_override is inaccurate: None currently only
resolves to SPV's confirmed tip when sync_progress() already has headers,
otherwise the code falls back to genesis (unwrap_or(0)); update behavior so the
public contract matches runtime behavior by either (a) changing the comment on
birth_height_override to state the real fallback (None -> use sync_progress()
tip if available, otherwise fall back to genesis), or (b) change the
implementation that resolves None to consult a durable/persisted header tip from
the SPV subsystem (e.g., call the persisted tip API instead of relying solely on
sync_progress()), then use that tip height as the default; update all places
referencing birth_height_override and sync_progress() accordingly (also fix
similar wording/behavior where birth_height_override is documented at the other
occurrences).

@lklimek lklimek added the ready for final review Ready for the final review. If AI was involved in producing this PR, it has already had a reviewer. label May 13, 2026
Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Production change correctly threads birth_height_override: Option<u32> through register_wallet so in-memory ManagedWalletInfo and persisted WalletMetadataEntry.birth_height share a single resolved value, closing the prior in-memory-vs-persisted drift bug. Three valid concerns remain: the public docstring on None claims a [H_now, ∞) scan but unwrap_or(0) silently degrades to a genesis rescan when SPV isn't running yet; no test pins the lock-step invariant or the persist/reload path; and the FFI shim hardcodes None, which is a real first-run behavioral change for iOS clients (pre-PR in-memory birth_height was hardcoded to 0 = genesis scan on first launch, post-PR FFI clients get SPV-tip-forward and silently miss pre-funded coins).

I validated existing CodeRabbit comments separately in-thread instead of restating them here.

Reviewed commit: 59d49f9

🟡 2 suggestion(s) | 💬 1 nitpick(s)

1 additional finding

🟡 suggestion: No test pins the in-memory / persisted lock-step or the restart-reload path

packages/rs-platform-wallet/tests/spv_sync.rs (lines 33-92)

The central invariant introduced by this PR is that one resolved birth_height flows into both ManagedWalletInfo (in-memory checkpoint) and the persisted WalletMetadataEntry, and survives reload. The only test change is spv_sync.rs passing None to compile against the new signature. RecordingPersister records only synced_height from core changesets, and its load() returns ClientStartState::default(), so the suite never asserts (a) that Some(h) ends up in the captured wallet_metadata.birth_height, (b) that the None branch resolves to the same value in both sinks, or (c) that the value round-trips through load_from_persistor into state.core_wallet.birth_height(). Extend RecordingPersister to capture changeset.wallet_metadata and add a focused test that calls create_wallet_from_seed_bytes(..., Some(42)) and asserts the recorded birth_height == 42 plus matching managed info; a second case with Some(0) and a third that feeds the captured state back through load() would lock the restart path the PR is explicitly fixing.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/manager.rs`:
- [SUGGESTION] lines 103-147: FFI hardcodes `None`, regressing first-run pre-funded mnemonic imports for iOS
  Both `platform_wallet_manager_create_wallet_from_seed` (line 104) and `platform_wallet_manager_create_wallet_from_mnemonic` (line 146) unconditionally pass `None` for `birth_height_override`. Before this PR, `ManagedWalletInfo` was hardcoded to `birth_height = 0` at registration, so an FFI caller's first-process SPV scan started at genesis and would surface coins funded before the wallet was registered. After this PR, `None` resolves to the current SPV header tip whenever SPV is up, so the same flow silently misses pre-existing UTXOs. This is exactly the failure mode the PR description cites as the motivation for the new override (bank-wallet pre-funded with testnet duffs), yet the override is unreachable from the FFI/Swift surface — which is the primary consumer of this crate. The pre-PR behaviour was already broken across restart (persisted birth_height was SPV-tip), so this is a tradeoff not a pure regression, but it leaves the stated use case unaddressable from iOS. Add a sibling `*_with_birth_height` FFI entry (or a `birth_height_override: i64` with sentinel) so iOS callers can opt into `Some(0)` / `Some(h)`. Not provided as a `suggestion` patch because exposing it cleanly requires an additional FFI export, not a one-line edit.

In `packages/rs-platform-wallet/tests/spv_sync.rs`:
- [SUGGESTION] lines 33-92: No test pins the in-memory / persisted lock-step or the restart-reload path
  The central invariant introduced by this PR is that one resolved `birth_height` flows into both `ManagedWalletInfo` (in-memory checkpoint) and the persisted `WalletMetadataEntry`, and survives reload. The only test change is `spv_sync.rs` passing `None` to compile against the new signature. `RecordingPersister` records only `synced_height` from core changesets, and its `load()` returns `ClientStartState::default()`, so the suite never asserts (a) that `Some(h)` ends up in the captured `wallet_metadata.birth_height`, (b) that the `None` branch resolves to the same value in both sinks, or (c) that the value round-trips through `load_from_persistor` into `state.core_wallet.birth_height()`. Extend `RecordingPersister` to capture `changeset.wallet_metadata` and add a focused test that calls `create_wallet_from_seed_bytes(..., Some(42))` and asserts the recorded `birth_height == 42` plus matching managed info; a second case with `Some(0)` and a third that feeds the captured state back through `load()` would lock the restart path the PR is explicitly fixing.

Comment on lines 103 to +147
@@ -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,
))
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.

🟡 Suggestion: FFI hardcodes None, regressing first-run pre-funded mnemonic imports for iOS

Both platform_wallet_manager_create_wallet_from_seed (line 104) and platform_wallet_manager_create_wallet_from_mnemonic (line 146) unconditionally pass None for birth_height_override. Before this PR, ManagedWalletInfo was hardcoded to birth_height = 0 at registration, so an FFI caller's first-process SPV scan started at genesis and would surface coins funded before the wallet was registered. After this PR, None resolves to the current SPV header tip whenever SPV is up, so the same flow silently misses pre-existing UTXOs. This is exactly the failure mode the PR description cites as the motivation for the new override (bank-wallet pre-funded with testnet duffs), yet the override is unreachable from the FFI/Swift surface — which is the primary consumer of this crate. The pre-PR behaviour was already broken across restart (persisted birth_height was SPV-tip), so this is a tradeoff not a pure regression, but it leaves the stated use case unaddressable from iOS. Add a sibling *_with_birth_height FFI entry (or a birth_height_override: i64 with sentinel) so iOS callers can opt into Some(0) / Some(h). Not provided as a suggestion patch because exposing it cleanly requires an additional FFI export, not a one-line edit.

source: ['claude', 'codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/manager.rs`:
- [SUGGESTION] lines 103-147: FFI hardcodes `None`, regressing first-run pre-funded mnemonic imports for iOS
  Both `platform_wallet_manager_create_wallet_from_seed` (line 104) and `platform_wallet_manager_create_wallet_from_mnemonic` (line 146) unconditionally pass `None` for `birth_height_override`. Before this PR, `ManagedWalletInfo` was hardcoded to `birth_height = 0` at registration, so an FFI caller's first-process SPV scan started at genesis and would surface coins funded before the wallet was registered. After this PR, `None` resolves to the current SPV header tip whenever SPV is up, so the same flow silently misses pre-existing UTXOs. This is exactly the failure mode the PR description cites as the motivation for the new override (bank-wallet pre-funded with testnet duffs), yet the override is unreachable from the FFI/Swift surface — which is the primary consumer of this crate. The pre-PR behaviour was already broken across restart (persisted birth_height was SPV-tip), so this is a tradeoff not a pure regression, but it leaves the stated use case unaddressable from iOS. Add a sibling `*_with_birth_height` FFI entry (or a `birth_height_override: i64` with sentinel) so iOS callers can opt into `Some(0)` / `Some(h)`. Not provided as a `suggestion` patch because exposing it cleanly requires an additional FFI export, not a one-line edit.

Comment on lines 70 to 111
@@ -70,35 +82,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` 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<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
}
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: Breaking change to public Rust API not surfaced in PR title

create_wallet_from_mnemonic and create_wallet_from_seed_bytes are pub and gain a non-defaulted Option<u32> parameter — any external Rust consumer of packages/rs-platform-wallet needs to migrate. Per .github/workflows/pr.yml (conventional-commits enforcement) and the PR template's breaking-change checkbox, this should be feat(rs-platform-wallet)!: so the API break surfaces in generated changelogs. The C ABI is preserved by the FFI shim (which absorbs the parameter as None), so this is purely about Rust-crate consumers and changelog hygiene. Nitpick within an internal monorepo where all callers are updated in-PR, but worth correcting before merge.

source: ['claude']

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

The core fix is correct: register_wallet now resolves one birth_height and writes the same value into both ManagedWalletInfo and WalletMetadataEntry, which removes the previous in-memory vs persisted drift. Three follow-up issues remain at this head: the public Rust docs overstate what None guarantees, the FFI/Swift boundary still hides the new override while changing None semantics once SPV is already running, and there is still no focused test that locks the new birth-height invariant across persistence and reload.

Reviewed commit: e246dd3

🟡 3 suggestion(s)

1 additional finding

🟡 suggestion: The new birth-height persistence invariant still has no focused test

packages/rs-platform-wallet/tests/spv_sync.rs (lines 33-90)

This test helper records only whether a core changeset existed and its synced_height, and load() always returns ClientStartState::default(). That means the suite never asserts the behavior this PR is supposed to protect: the resolved birth_height written into ManagedWalletInfo must match WalletMetadataEntry.birth_height and survive a load_from_persistor() round-trip. Because the original bug was precisely a drift between in-memory and persisted birth height, leaving that invariant untested makes this regression easy to reintroduce during future refactors.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- [SUGGESTION] lines 60-69: Public `None` contract omits the fallback to a genesis scan
  The rustdoc says `birth_height_override = None` seeds the wallet from SPV's current confirmed tip, but `register_wallet` does not guarantee that. The actual resolver at lines 132-139 uses `sync_progress().await...unwrap_or(0)`, so when SPV is not running yet or header access fails, `None` falls back to `0` and triggers a full historical scan from genesis. This is not just an internal detail: callers choose materially different sync cost and discovery behavior through this API, so the public contract needs to describe the offline-SPV fallback accurately.

In `packages/rs-platform-wallet-ffi/src/manager.rs`:
- [SUGGESTION] lines 103-147: FFI constructors hardcode `None`, so Swift cannot request the new recovery scan modes
  Both exported constructors still pass `birth_height_override = None` into the Rust API, and the public Swift wrappers expose no parameter for `Some(0)` or `Some(h)`. That would be a pure ergonomics gap if `None` still behaved exactly as before, but this PR changed the underlying semantics: before, wallet registration always seeded `ManagedWalletInfo` with `0`; now `None` resolves to the live SPV tip whenever SPV is already running. Swift can start SPV before wallet creation via `PlatformWalletManagerSPV.startSpv`, so this path is reachable from public API. As a result, the main FFI consumer cannot force the full-history or known-height rescan that this feature was added to support, and the commit message's claim that FFI `None` preserves existing semantics is not true once SPV has started.

In `packages/rs-platform-wallet/tests/spv_sync.rs`:
- [SUGGESTION] lines 33-90: The new birth-height persistence invariant still has no focused test
  This test helper records only whether a core changeset existed and its `synced_height`, and `load()` always returns `ClientStartState::default()`. That means the suite never asserts the behavior this PR is supposed to protect: the resolved `birth_height` written into `ManagedWalletInfo` must match `WalletMetadataEntry.birth_height` and survive a `load_from_persistor()` round-trip. Because the original bug was precisely a drift between in-memory and persisted birth height, leaving that invariant untested makes this regression easy to reintroduce during future refactors.

Comment on lines +60 to +69
/// `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.
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.

🟡 Suggestion: Public None contract omits the fallback to a genesis scan

The rustdoc says birth_height_override = None seeds the wallet from SPV's current confirmed tip, but register_wallet does not guarantee that. The actual resolver at lines 132-139 uses sync_progress().await...unwrap_or(0), so when SPV is not running yet or header access fails, None falls back to 0 and triggers a full historical scan from genesis. This is not just an internal detail: callers choose materially different sync cost and discovery behavior through this API, so the public contract needs to describe the offline-SPV fallback accurately.

💡 Suggested change
Suggested change
/// `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.
/// `birth_height_override` controls SPV's compact-filter scan
/// window for the new wallet. `None` prefers SPV's current
/// confirmed header tip so the scan window is `[H_now, ∞)`, but
/// falls back to `0` (full historical scan from genesis) when SPV
/// state is unavailable. `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.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- [SUGGESTION] lines 60-69: Public `None` contract omits the fallback to a genesis scan
  The rustdoc says `birth_height_override = None` seeds the wallet from SPV's current confirmed tip, but `register_wallet` does not guarantee that. The actual resolver at lines 132-139 uses `sync_progress().await...unwrap_or(0)`, so when SPV is not running yet or header access fails, `None` falls back to `0` and triggers a full historical scan from genesis. This is not just an internal detail: callers choose materially different sync cost and discovery behavior through this API, so the public contract needs to describe the offline-SPV fallback accurately.

Comment on lines 103 to +147
@@ -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,
))
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.

🟡 Suggestion: FFI constructors hardcode None, so Swift cannot request the new recovery scan modes

Both exported constructors still pass birth_height_override = None into the Rust API, and the public Swift wrappers expose no parameter for Some(0) or Some(h). That would be a pure ergonomics gap if None still behaved exactly as before, but this PR changed the underlying semantics: before, wallet registration always seeded ManagedWalletInfo with 0; now None resolves to the live SPV tip whenever SPV is already running. Swift can start SPV before wallet creation via PlatformWalletManagerSPV.startSpv, so this path is reachable from public API. As a result, the main FFI consumer cannot force the full-history or known-height rescan that this feature was added to support, and the commit message's claim that FFI None preserves existing semantics is not true once SPV has started.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet-ffi/src/manager.rs`:
- [SUGGESTION] lines 103-147: FFI constructors hardcode `None`, so Swift cannot request the new recovery scan modes
  Both exported constructors still pass `birth_height_override = None` into the Rust API, and the public Swift wrappers expose no parameter for `Some(0)` or `Some(h)`. That would be a pure ergonomics gap if `None` still behaved exactly as before, but this PR changed the underlying semantics: before, wallet registration always seeded `ManagedWalletInfo` with `0`; now `None` resolves to the live SPV tip whenever SPV is already running. Swift can start SPV before wallet creation via `PlatformWalletManagerSPV.startSpv`, so this path is reachable from public API. As a result, the main FFI consumer cannot force the full-history or known-height rescan that this feature was added to support, and the commit message's claim that FFI `None` preserves existing semantics is not true once SPV has started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for final review Ready for the final review. If AI was involved in producing this PR, it has already had a reviewer.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants