Skip to content

feat(platform-wallet): external signable wallets#3639

Open
ZocoLini wants to merge 1 commit into
v3.1-devfrom
feat/external-signable-wallets
Open

feat(platform-wallet): external signable wallets#3639
ZocoLini wants to merge 1 commit into
v3.1-devfrom
feat/external-signable-wallets

Conversation

@ZocoLini
Copy link
Copy Markdown
Collaborator

@ZocoLini ZocoLini commented May 13, 2026

After Wallet::from_mnemonic and Wallet::from_seed I do a wallet downgrade into ExternableSignable, forcing every transaction to be signed externally

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

Summary by CodeRabbit

  • Refactor
    • Improved wallet creation flow to ensure wallets are prepared in a safer, external-signable state before registration.
  • Chores
    • Updated underlying library revisions to align core platform components.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a1609db-5cb0-4907-9578-6012059e2c32

📥 Commits

Reviewing files that changed from the base of the PR and between 9f28fe6 and f4f4d29.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • Cargo.toml
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs

📝 Walkthrough

Walkthrough

This PR downgrades freshly-created Wallets to an external-signable form before registration and updates several rust-dashcore Git dependency revisions in the workspace Cargo.toml.

Changes

Wallet External-Signable Downgrade & Workspace Rev Bump

Layer / File(s) Summary
Downgrade freshly-created wallets before registration
packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
create_wallet_from_mnemonic and create_wallet_from_seed_bytes now bind the created Wallet to a mutable variable, call wallet.downgrade_to_external_signable(), and pass the resulting external-signable wallet into register_wallet.
Workspace dependency revision bump
Cargo.toml
Git rev for dashcore-derived workspace dependencies (dashcore, dash-network-seeds, dash-spv, key-wallet, key-wallet-ffi, key-wallet-manager, dash-network, dashcore-rpc) was updated to a new shared revision.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble code in moonlit halls,
I hop through changes, big and small,
Wallets shed secrets, tidy and neat,
Dependencies marched in orderly feet—
A rabbit's cheer for builds complete! 🐇✨

🚥 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 title accurately reflects the main change: wallets are now downgraded to external-signable form immediately after creation, which is the primary modification across the codebase.
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/external-signable-wallets

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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-05-13T17:57:17.078Z

@ZocoLini ZocoLini force-pushed the feat/external-signable-wallets branch 2 times, most recently from dc7dafb to e3473a5 Compare May 19, 2026 17:30
@ZocoLini ZocoLini changed the title feat: external signable wallets and tx building with signer feat: external signable wallets May 19, 2026
@ZocoLini ZocoLini marked this pull request as ready for review May 19, 2026 19:24
@ZocoLini ZocoLini requested a review from QuantumExplorer as a code owner May 19, 2026 19:24
@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.15%. Comparing base (db8fb08) to head (f4f4d29).
⚠️ Report is 1 commits behind head on v3.1-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3639      +/-   ##
============================================
- Coverage     87.15%   87.15%   -0.01%     
============================================
  Files          2606     2606              
  Lines        319221   319221              
============================================
- Hits         278216   278215       -1     
- Misses        41005    41006       +1     
Components Coverage Δ
dpp 87.67% <ø> (-0.01%) ⬇️
drive 85.95% <ø> (ø)
drive-abci 89.60% <ø> (ø)
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 92.17% <ø> (ø)
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 49.16% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 19, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "8e580a55cf225cceefbeeaf6b332f9ebd711889902b298fa5b0309cbb03c12c2"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

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

One-file change that ensures create_wallet_from_mnemonic / create_wallet_from_seed_bytes always yield an ExternalSignable wallet. The type-level invariant is correctly enforced, but the helper's documented security guarantee ("strip the in-process xpriv") is not actually delivered: drop(temp) runs no zeroization because key_wallet::Wallet has only a manual Zeroize impl, no Drop/ZeroizeOnDrop. The exact same downgrade path upstream in key-wallet-manager calls wallet.zeroize() before drop. Two secondary issues: the cloned per-account is_watch_only flag diverges from how the restore path reconstructs the same wallet, and the new invariant has no test coverage.

🟡 3 suggestion(s)

Comment thread packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs Outdated
Comment thread packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs Outdated
Comment thread packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
@ZocoLini ZocoLini force-pushed the feat/external-signable-wallets branch from e3473a5 to 9f28fe6 Compare May 20, 2026 19:23
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.

♻️ Duplicate comments (2)
packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs (2)

427-429: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

External-signable wallet may retain non-watch-only account flags.

At Line 427, cloning accounts from an xpriv-derived wallet can preserve is_watch_only=false while wallet type becomes ExternalSignable, which can diverge from restore-path semantics and trigger fragile account behavior.

Suggested fix
 fn downgrade_to_external_signable(mut temp: Wallet) -> Wallet {
     use zeroize::Zeroize;
     let wallet_id = temp.wallet_id;
     let network = temp.network;
-    let accounts = temp.accounts.clone();
+    let mut accounts = temp.accounts.clone();
+    for account in accounts.all_accounts_mut() {
+        account.to_watch_only();
+    }
     temp.zeroize();
     drop(temp);
     Wallet::new_external_signable(network, wallet_id, accounts)
 }
#!/bin/bash
# Verify watch-only semantics across creation and restore paths.
set -euo pipefail

rg -n -C3 'from_xpriv|from_xpub|is_watch_only|to_watch_only' --type rust
rg -n -C3 'new_external_signable|new_watch_only|create_wallet_from_mnemonic|create_wallet_from_seed_bytes' --type rust
🤖 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 427
- 429, Cloning accounts from the temporary xpriv-derived wallet into
Wallet::new_external_signable can carry over is_watch_only=false; update the
code that prepares accounts (the temp.accounts.clone usage) to convert each
account to watch-only semantics before passing them to
Wallet::new_external_signable (e.g., map/iterate the cloned accounts and set
their is_watch_only flag or call an existing to_watch_only on account structs),
ensuring ExternalSignable wallets never retain non-watch-only account flags and
align with the restore-path behavior.

424-429: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

drop(temp) does not scrub wallet secrets before deallocation.

At Line 428, the helper drops the temporary wallet without explicit zeroization. If Wallet is not ZeroizeOnDrop, mnemonic/seed-derived material can remain in freed heap pages.

Suggested fix
-fn downgrade_to_external_signable(temp: Wallet) -> Wallet {
+fn downgrade_to_external_signable(mut temp: Wallet) -> Wallet {
+    use zeroize::Zeroize;
     let wallet_id = temp.wallet_id;
     let network = temp.network;
     let accounts = temp.accounts.clone();
+    temp.zeroize();
     drop(temp);
     Wallet::new_external_signable(network, wallet_id, accounts)
}
#!/bin/bash
# Verify Wallet zeroization traits/impls and upstream downgrade behavior.
set -euo pipefail

rg -n -C3 'impl\s+Zeroize\s+for\s+Wallet|impl\s+Drop\s+for\s+Wallet|ZeroizeOnDrop' --type rust
rg -n -C3 'downgrade_to_external_signable|new_external_signable|zeroize\(' --type rust
🤖 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 424
- 429, The helper downgrade_to_external_signable currently calls drop(temp)
which does not guarantee zeroization of sensitive fields; before dropping the
temporary Wallet, explicitly scrub its secret material (e.g., call
temp.zeroize() or temp.wipe_sensitive() if such methods exist and Wallet
implements Zeroize) or, if not present, implement Zeroize/ZeroizeOnDrop (or a
Drop impl that zeroes mnemonic/seed fields) on Wallet so secrets are overwritten
before deallocation; ensure the change happens in downgrade_to_external_signable
(replace the drop(temp) call with an explicit zeroization call or rely on
Wallet's ZeroizeOnDrop) and keep Wallet::new_external_signable usage unchanged.
🤖 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.

Duplicate comments:
In `@packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:
- Around line 427-429: Cloning accounts from the temporary xpriv-derived wallet
into Wallet::new_external_signable can carry over is_watch_only=false; update
the code that prepares accounts (the temp.accounts.clone usage) to convert each
account to watch-only semantics before passing them to
Wallet::new_external_signable (e.g., map/iterate the cloned accounts and set
their is_watch_only flag or call an existing to_watch_only on account structs),
ensuring ExternalSignable wallets never retain non-watch-only account flags and
align with the restore-path behavior.
- Around line 424-429: The helper downgrade_to_external_signable currently calls
drop(temp) which does not guarantee zeroization of sensitive fields; before
dropping the temporary Wallet, explicitly scrub its secret material (e.g., call
temp.zeroize() or temp.wipe_sensitive() if such methods exist and Wallet
implements Zeroize) or, if not present, implement Zeroize/ZeroizeOnDrop (or a
Drop impl that zeroes mnemonic/seed fields) on Wallet so secrets are overwritten
before deallocation; ensure the change happens in downgrade_to_external_signable
(replace the drop(temp) call with an explicit zeroization call or rely on
Wallet's ZeroizeOnDrop) and keep Wallet::new_external_signable usage unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8908f0c-14e6-489b-90c3-ffb9e70fcda0

📥 Commits

Reviewing files that changed from the base of the PR and between 7b48e43 and 9f28fe6.

📒 Files selected for processing (1)
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs

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

PR #3639 makes mnemonic/seed-derived wallets get downgraded to ExternalSignable via a helper that clones the account collection and drops the temporary wallet. Two prior findings remain valid on the current head: drop(temp) does not actually zeroize the xpriv/seed material the doc-comment promises to strip (Wallet has a manual Zeroize impl but no Drop/ZeroizeOnDrop), and there is no test asserting the new ExternalSignable invariant on either creation entry point. The account-level is_watch_only mismatch is contested and not actionable: ExternalSignable semantically differs from WatchOnly (accounts can still sign via an external signer), and signing/zeroize decisions are driven by wallet_type rather than the account flag, so it is reclassified as resolved.

Note: Inline posting failed (command failed (1): gh api /repos/dashpay/platform/pulls/3639/reviews --method POST --input -
STDOUT:
{"message":"Unprocessable Entity","errors":["Variable $threads of type [DraftPullRequestReviewThread] was provided invalid value for 0.commitId (Field is not defined on DraftPullRequestReviewThread)), so I posted the same verified findings as a top-level review body.

🟡 2 suggestion(s)

2 additional finding(s)

suggestion: `drop(temp)` does not zeroize the xpriv the doc-comment promises to strip

packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs (line 420)

The doc-comment on downgrade_to_external_signable states the helper exists to "Strip the in-process xpriv from a freshly-derived wallet," but the implementation only clones accounts and then calls drop(temp). key_wallet::Wallet defines a manual impl Zeroize (key-wallet/src/wallet/mod.rs:153) which clears the mnemonic, seed, and chain code, but it has no Drop and is not annotated with ZeroizeOnDrop. The default destructor therefore just recursively drops fields — the mnemonic bytes, the RootExtendedPrivKey (including the secp256k1::SecretKey), the BIP39 seed, and the chain code all linger on the heap until something else reuses that allocation. For the threat model the PR is targeting ("seed lives only in the host's secure storage and the resolver vtable is the only way to sign"), the in-process secret is still recoverable from a heap dump, core file, swap, or attached debugger after this helper returns. Either explicitly call temp.zeroize() before/instead of drop(temp), or downgrade the doc-comment to reflect what the code actually does. Note that even Wallet::zeroize itself does not clear the inner secp256k1::SecretKey (key-wallet/src/wallet/mod.rs:168), and the originating Wallet::from_mnemonic/from_seed_bytes paths leave additional ephemeral copies of the seed in their own stack frames — so a complete fix likely also needs upstream cooperation, but temp.zeroize() is still strictly better than drop(temp).

fn downgrade_to_external_signable(mut temp: Wallet) -> Wallet {
    use zeroize::Zeroize;
    let wallet_id = temp.wallet_id;
    let network = temp.network;
    let accounts = temp.accounts.clone();
    temp.zeroize();
    drop(temp);
    Wallet::new_external_signable(network, wallet_id, accounts)
}
suggestion: New `ExternalSignable` invariant has no direct regression test

packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs (line 73)

The behavioral guarantee at the heart of this PR — that create_wallet_from_mnemonic and create_wallet_from_seed_bytes always return an ExternalSignable wallet rather than a mnemonic/seed-backed one — is not asserted by any test in the crate. The only call site under tests is packages/rs-platform-wallet/tests/spv_sync.rs, which exercises wallet creation purely as setup for SPV sync and never inspects wallet_type, is_external_signable(), or the account material. A grep for wallet_type / is_external_signable / is_watch_only under packages/rs-platform-wallet/tests returns nothing. A small unit test that calls each entry point and asserts wallet.is_external_signable() && !wallet.has_mnemonic() (plus that the per-account xpubs survived the downgrade) would lock in the invariant against a future refactor accidentally removing the downgrade_to_external_signable call or returning the seed-backed wallet again. The same test is the natural place to also verify that birth_height_override lands in the persisted WalletMetadataEntry.

🤖 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.

- [SUGGESTION] In `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:420-430: `drop(temp)` does not zeroize the xpriv the doc-comment promises to strip
  The doc-comment on `downgrade_to_external_signable` states the helper exists to "Strip the in-process xpriv from a freshly-derived wallet," but the implementation only clones `accounts` and then calls `drop(temp)`. `key_wallet::Wallet` defines a manual `impl Zeroize` (key-wallet/src/wallet/mod.rs:153) which clears the mnemonic, seed, and chain code, but it has no `Drop` and is not annotated with `ZeroizeOnDrop`. The default destructor therefore just recursively drops fields — the mnemonic bytes, the `RootExtendedPrivKey` (including the `secp256k1::SecretKey`), the BIP39 `seed`, and the chain code all linger on the heap until something else reuses that allocation. For the threat model the PR is targeting ("seed lives only in the host's secure storage and the resolver vtable is the only way to sign"), the in-process secret is still recoverable from a heap dump, core file, swap, or attached debugger after this helper returns. Either explicitly call `temp.zeroize()` before/instead of `drop(temp)`, or downgrade the doc-comment to reflect what the code actually does. Note that even `Wallet::zeroize` itself does not clear the inner `secp256k1::SecretKey` (key-wallet/src/wallet/mod.rs:168), and the originating `Wallet::from_mnemonic`/`from_seed_bytes` paths leave additional ephemeral copies of the seed in their own stack frames — so a complete fix likely also needs upstream cooperation, but `temp.zeroize()` is still strictly better than `drop(temp)`.
- [SUGGESTION] In `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:73-116: New `ExternalSignable` invariant has no direct regression test
  The behavioral guarantee at the heart of this PR — that `create_wallet_from_mnemonic` and `create_wallet_from_seed_bytes` always return an `ExternalSignable` wallet rather than a mnemonic/seed-backed one — is not asserted by any test in the crate. The only call site under tests is `packages/rs-platform-wallet/tests/spv_sync.rs`, which exercises wallet creation purely as setup for SPV sync and never inspects `wallet_type`, `is_external_signable()`, or the account material. A grep for `wallet_type` / `is_external_signable` / `is_watch_only` under `packages/rs-platform-wallet/tests` returns nothing. A small unit test that calls each entry point and asserts `wallet.is_external_signable() && !wallet.has_mnemonic()` (plus that the per-account xpubs survived the downgrade) would lock in the invariant against a future refactor accidentally removing the `downgrade_to_external_signable` call or returning the seed-backed wallet again. The same test is the natural place to also verify that `birth_height_override` lands in the persisted `WalletMetadataEntry`.

@ZocoLini ZocoLini changed the title feat: external signable wallets feat(platform-wallet): external signable wallets May 20, 2026
e
))
})?;
let wallet = downgrade_to_external_signable(wallet);
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.

Why we aren't doing this in from_mnemonic or from_seed_bytes in rust-dashcore?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

bcs, so far, right now we use and need internal singable wallets for the tests, and I don't know if we want to only have external singable wallets exposed, it is something that can be done, removing the WalletType field in the Wallet struct shouldn't be that hard, tests would need and update but thats it

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.

This logic is on the rust-dashcore side; we shouldn't patch it here. You'd better make separate methods for testing only purposes that are available only for test builds.

Copy link
Copy Markdown
Collaborator Author

@ZocoLini ZocoLini May 22, 2026

Choose a reason for hiding this comment

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

I mean, is not like we made this for testing, it was already there when I joined the company xd, but yeah, as I said, I can drop it, shouldn't be that big of a deal

Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

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

Talked with Ivan, we think it's best to have create_wallet_from_mnemonic take a bool for downgrading here. And add a downgrade method in rust dash core.

@ZocoLini
Copy link
Copy Markdown
Collaborator Author

okay

After the colleague's MnemonicResolverCoreSigner landed every signing
path is signer-driven, so freshly-derived wallets no longer need to
keep the root xpriv in-process. `create_wallet_from_mnemonic` and
`create_wallet_from_seed_bytes` now derive accounts via
`Wallet::from_mnemonic`/`from_seed_bytes` and immediately downgrade
to an ExternalSignable wallet with the same wallet_id + account xpubs.
The seed stays in the host's secure storage and the resolver vtable
remains the only way to sign.
@ZocoLini ZocoLini force-pushed the feat/external-signable-wallets branch from 9f28fe6 to f4f4d29 Compare May 23, 2026 01:05
@ZocoLini
Copy link
Copy Markdown
Collaborator Author

I updated the PR, and honestly, right now it doesn't make sense to me to add that boolean arg to the function if we are always setting it to true

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 actual PR delta is a 6-line change in packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs that calls wallet.downgrade_to_external_signable() after Wallet::from_mnemonic / Wallet::from_seed_bytes so the in-process xpriv is dropped immediately and signing must flow through the external signer vtable. Prior finding prior-1 (drop(temp) doesn't zeroize) is RESOLVED — the local clone+drop helper was replaced with a direct call to upstream Wallet::downgrade_to_external_signable(), which relies on standard Rust Drop chaining into key_wallet's ZeroizeOnDrop impls. Prior finding prior-2 (missing regression test for the ExternalSignable invariant) is STILL VALID and is carried forward as the only material residual issue; a docs-coverage suggestion is added.

Note: Inline posting failed (command failed (1): gh api /repos/dashpay/platform/pulls/3639/reviews --method POST --input -
STDOUT:
{"message":"Unprocessable Entity","errors":["Variable $threads of type [DraftPullRequestReviewThread] was provided invalid value for 0.commitId (Field is not defined on DraftPullRequestReviewThread)), so I posted the same verified findings as a top-level review body.

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

2 additional finding(s)

suggestion: No regression test pins the new ExternalSignable invariant

packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs (line 73)

The behavioral guarantee at the heart of this PR — that create_wallet_from_mnemonic and create_wallet_from_seed_bytes always return a wallet whose wallet_type == ExternalSignable and which holds no in-process xpriv — is not asserted by any test in packages/rs-platform-wallet. Grep finds only two call sites for these constructors (tests/spv_sync.rs:184 and examples/basic_usage.rs:59), and neither inspects the returned wallet's type or signing capabilities. The only thing keeping the guarantee alive is the single wallet.downgrade_to_external_signable(); line at wallet_lifecycle.rs:88 and :114; a future refactor that removes, reorders, or skips that call for one of the two constructors will silently regress the security model — the wallet will still work, SPV will still scan, and the only observable difference is that the xpriv now lives in process memory rather than going through the external resolver. Add a unit/integration test for each entry point that asserts wallet.is_external_signable() on the returned/managed wallet (and ideally that signing without an external resolver fails), so the invariant is locked in at the public API boundary. Carried forward from prior review at 9f28fe6 where the same gap was flagged.

nitpick: Doc-comments on the two creation entry points don't mention the ExternalSignable downgrade

packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs (line 52)

The doc-comments on create_wallet_from_mnemonic (52–72) and create_wallet_from_seed_bytes (92–100) cover language detection and birth-height semantics but say nothing about the load-bearing behavior introduced by this PR — that the returned wallet is WalletType::ExternalSignable and therefore holds no in-process xpriv. A caller reading the signature and docs would reasonably assume that feeding in a mnemonic produces a wallet that can sign from in-process key material, which is no longer true: the design now requires the host to register a MnemonicResolverCoreSigner (or equivalent) before any signing attempt, otherwise signing will fail at runtime. Document that contract at the public API surface so callers don't waste time debugging a 'wallet won't sign' error.

🤖 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.

- [SUGGESTION] In `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:73-116: No regression test pins the new ExternalSignable invariant
  The behavioral guarantee at the heart of this PR — that `create_wallet_from_mnemonic` and `create_wallet_from_seed_bytes` always return a wallet whose `wallet_type == ExternalSignable` and which holds no in-process xpriv — is not asserted by any test in `packages/rs-platform-wallet`. Grep finds only two call sites for these constructors (`tests/spv_sync.rs:184` and `examples/basic_usage.rs:59`), and neither inspects the returned wallet's type or signing capabilities. The only thing keeping the guarantee alive is the single `wallet.downgrade_to_external_signable();` line at wallet_lifecycle.rs:88 and :114; a future refactor that removes, reorders, or skips that call for one of the two constructors will silently regress the security model — the wallet will still work, SPV will still scan, and the only observable difference is that the xpriv now lives in process memory rather than going through the external resolver. Add a unit/integration test for each entry point that asserts `wallet.is_external_signable()` on the returned/managed wallet (and ideally that signing without an external resolver fails), so the invariant is locked in at the public API boundary. Carried forward from prior review at 9f28fe6e where the same gap was flagged.
- [NITPICK] In `packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs`:52-107: Doc-comments on the two creation entry points don't mention the ExternalSignable downgrade
  The doc-comments on `create_wallet_from_mnemonic` (52–72) and `create_wallet_from_seed_bytes` (92–100) cover language detection and birth-height semantics but say nothing about the load-bearing behavior introduced by this PR — that the returned wallet is `WalletType::ExternalSignable` and therefore holds no in-process xpriv. A caller reading the signature and docs would reasonably assume that feeding in a mnemonic produces a wallet that can sign from in-process key material, which is no longer true: the design now requires the host to register a `MnemonicResolverCoreSigner` (or equivalent) before any signing attempt, otherwise signing will fail at runtime. Document that contract at the public API surface so callers don't waste time debugging a 'wallet won't sign' error.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants