From b32c29f912a3a2f590207be6d19198ec4414d0e3 Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Wed, 19 Aug 2026 00:46:41 -0400 Subject: [PATCH] Return an error from `NodeEntropy::from_bip39_mnemonic` in bindings The uniffi-exposed `from_bip39_mnemonic` constructor was infallible and took the `Mnemonic` custom type, which parses the string during argument lifting. An invalid mnemonic would fail the lift and surface as an unexpected-error call status. The generated Swift wrapper for an infallible function wraps the call in `try!`, so passing an invalid mnemonic (e.g., a user typo during wallet restore) aborts the process with an uncatchable EXC_BREAKPOINT. Kotlin and Python raise their generic internal exceptions instead. Following the `from_seed_bytes` precedent, give the constructor a uniffi-specific signature that takes the mnemonic as a plain string and returns `Result`, parsing inside the function and reporting failures via a new `EntropyError::InvalidMnemonic` variant. The non-uniffi Rust API is unchanged. --- src/entropy.rs | 44 ++++++++++++++++++++++++++++++++++++++++---- tests/common/mod.rs | 3 +++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/entropy.rs b/src/entropy.rs index 2f7faa1b4..5ec596ed2 100644 --- a/src/entropy.rs +++ b/src/entropy.rs @@ -19,6 +19,8 @@ use crate::io; #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "uniffi", derive(uniffi::Error))] pub enum EntropyError { + /// The given BIP 39 mnemonic is invalid. + InvalidMnemonic, /// The given seed bytes are invalid, e.g., have invalid length. InvalidSeedBytes, /// The given seed file is invalid, e.g., has invalid length, or could not be read. @@ -28,6 +30,7 @@ pub enum EntropyError { impl fmt::Display for EntropyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { + Self::InvalidMnemonic => write!(f, "Given BIP 39 mnemonic is invalid."), Self::InvalidSeedBytes => write!(f, "Given seed bytes are invalid."), Self::InvalidSeedFile => write!(f, "Given seed file is invalid or could not be read."), } @@ -45,6 +48,18 @@ impl std::error::Error for EntropyError {} pub struct NodeEntropy([u8; WALLET_KEYS_SEED_LEN]); impl NodeEntropy { + /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. + /// + /// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki + /// [`Node`]: crate::Node + #[cfg(not(feature = "uniffi"))] + pub fn from_bip39_mnemonic(mnemonic: Mnemonic, passphrase: Option) -> Self { + match passphrase { + Some(passphrase) => Self(mnemonic.to_seed(passphrase)), + None => Self(mnemonic.to_seed("")), + } + } + /// Configures the [`Node`] instance to source its wallet entropy from the given /// [`WALLET_KEYS_SEED_LEN`] seed bytes. /// @@ -63,13 +78,19 @@ impl NodeEntropy { impl NodeEntropy { /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. /// + /// Will return an error if the given mnemonic is invalid. + /// /// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki /// [`Node`]: crate::Node - #[cfg_attr(feature = "uniffi", uniffi::constructor)] - pub fn from_bip39_mnemonic(mnemonic: Mnemonic, passphrase: Option) -> Self { + #[cfg(feature = "uniffi")] + #[uniffi::constructor] + pub fn from_bip39_mnemonic( + mnemonic: String, passphrase: Option, + ) -> Result { + let mnemonic = Mnemonic::parse(&mnemonic).map_err(|_| EntropyError::InvalidMnemonic)?; match passphrase { - Some(passphrase) => Self(mnemonic.to_seed(passphrase)), - None => Self(mnemonic.to_seed("")), + Some(passphrase) => Ok(Self(mnemonic.to_seed(passphrase))), + None => Ok(Self(mnemonic.to_seed(""))), } } @@ -166,6 +187,21 @@ impl WordCount { mod tests { use super::*; + #[cfg(feature = "uniffi")] + #[test] + fn invalid_mnemonic_returns_error() { + // A bad checksum must yield an error rather than an argument-lift panic that + // aborts the process in the Swift bindings. + let invalid = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"; + assert_eq!( + NodeEntropy::from_bip39_mnemonic(invalid.to_string(), None).err(), + Some(EntropyError::InvalidMnemonic) + ); + + let valid = generate_entropy_mnemonic(None); + assert!(NodeEntropy::from_bip39_mnemonic(valid.to_string(), None).is_ok()); + } + #[test] fn mnemonic_to_entropy_to_mnemonic() { // Test default (24 words) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9c1bb75c0..a08ed933c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -591,6 +591,9 @@ impl Default for TestConfig { let store_type = Default::default(); let mnemonic = generate_entropy_mnemonic(None); + #[cfg(feature = "uniffi")] + let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic.to_string(), None).unwrap(); + #[cfg(not(feature = "uniffi"))] let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); let async_payments_role = None; let wallet_rescan_from_height = None;