Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions key-wallet-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Functions: 63
| `wallet_build_and_sign_transaction` | Build and sign a transaction using the wallet's managed info This is the... | transaction |
| `wallet_check_transaction` | Check if a transaction belongs to the wallet using ManagedWalletInfo #... | transaction |
| `wallet_create_from_mnemonic` | Create a new wallet from mnemonic (backward compatibility - single network) ... | wallet |
| `wallet_create_from_mnemonic_with_options` | Create a new wallet from mnemonic with options # Safety - `mnemonic` must... | wallet |
| `wallet_create_from_mnemonic_with_options` | Create a new wallet from mnemonic with options The mnemonic may be in any... | wallet |
| `wallet_create_from_seed` | Create a new wallet from seed (backward compatibility) # Safety - `seed`... | wallet |
| `wallet_create_from_seed_with_options` | Create a new wallet from seed with options # Safety - `seed` must be a... | wallet |
| `wallet_create_random` | Create a new random wallet (backward compatibility) # Safety - `error`... | wallet |
Expand Down Expand Up @@ -324,8 +324,8 @@ Functions: 6
| `mnemonic_free` | Free a mnemonic string # Safety - `mnemonic` must be a valid pointer... | mnemonic |
| `mnemonic_generate` | Generate a new mnemonic with specified word count (12, 15, 18, 21, or 24) #... | mnemonic |
| `mnemonic_generate_with_language` | Generate a new mnemonic with specified language and word count # Safety ... | mnemonic |
| `mnemonic_to_seed` | Convert mnemonic to seed with optional passphrase # Safety - `mnemonic`... | mnemonic |
| `mnemonic_validate` | Validate a mnemonic phrase # Safety - `mnemonic` must be a valid... | mnemonic |
| `mnemonic_to_seed` | Convert mnemonic to seed with optional passphrase The mnemonic may be in... | mnemonic |
| `mnemonic_validate` | Validate a mnemonic phrase in any supported BIP-39 language | mnemonic |
| `mnemonic_word_count` | Get word count from mnemonic # Safety - `mnemonic` must be a valid... | mnemonic |

### Utility Functions
Expand Down Expand Up @@ -1357,7 +1357,7 @@ wallet_create_from_mnemonic(mnemonic: *const c_char, network: FFINetwork, error:
```

**Description:**
Create a new wallet from mnemonic (backward compatibility - single network) # Safety - `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed
Create a new wallet from mnemonic (backward compatibility - single network) The mnemonic may be in any supported BIP-39 language (detected automatically). # Safety - `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed

**Safety:**
- `mnemonic` must be a valid pointer to a null-terminated C string - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed
Expand All @@ -1373,7 +1373,7 @@ wallet_create_from_mnemonic_with_options(mnemonic: *const c_char, network: FFINe
```

**Description:**
Create a new wallet from mnemonic with options # Safety - `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed
Create a new wallet from mnemonic with options The mnemonic may be in any supported BIP-39 language (detected automatically). # Safety - `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed

**Safety:**
- `mnemonic` must be a valid pointer to a null-terminated C string - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null - `error` must be a valid pointer to an FFIError structure - The caller must ensure all pointers remain valid for the duration of this call - The returned pointer must be freed with `wallet_free` when no longer needed
Expand Down Expand Up @@ -2207,7 +2207,7 @@ account_derive_extended_private_key_from_mnemonic(account: *const FFIAccount, mn
```

**Description:**
Derive an extended private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError
Derive an extended private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`. Accepts a mnemonic in any supported BIP-39 language (detected automatically). # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError

**Safety:**
- `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError
Expand Down Expand Up @@ -2271,7 +2271,7 @@ account_derive_private_key_from_mnemonic(account: *const FFIAccount, mnemonic: *
```

**Description:**
Derive a private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError
Derive a private key from a mnemonic + optional passphrase at the given index. Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`. Accepts a mnemonic in any supported BIP-39 language (detected automatically). # Safety - `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError

**Safety:**
- `account` must be a valid pointer to an FFIAccount - `mnemonic` must be a valid, null-terminated C string - `passphrase` may be null; if not null, must be a valid C string - `error` must be a valid pointer to an FFIError
Expand Down Expand Up @@ -4112,7 +4112,7 @@ mnemonic_to_seed(mnemonic: *const c_char, passphrase: *const c_char, seed_out: *
```

**Description:**
Convert mnemonic to seed with optional passphrase # Safety - `mnemonic` must be a valid null-terminated C string - `passphrase` must be a valid null-terminated C string or null - `seed_out` must be a valid pointer to a buffer of at least 64 bytes - `seed_len` must be a valid pointer to store the seed length - `error` must be a valid pointer to an FFIError
Convert mnemonic to seed with optional passphrase The mnemonic may be in any supported BIP-39 language (detected automatically). # Safety - `mnemonic` must be a valid null-terminated C string - `passphrase` must be a valid null-terminated C string or null - `seed_out` must be a valid pointer to a buffer of at least 64 bytes - `seed_len` must be a valid pointer to store the seed length - `error` must be a valid pointer to an FFIError

**Safety:**
- `mnemonic` must be a valid null-terminated C string - `passphrase` must be a valid null-terminated C string or null - `seed_out` must be a valid pointer to a buffer of at least 64 bytes - `seed_len` must be a valid pointer to store the seed length - `error` must be a valid pointer to an FFIError
Expand All @@ -4128,7 +4128,7 @@ mnemonic_validate(mnemonic: *const c_char, error: *mut FFIError) -> bool
```

**Description:**
Validate a mnemonic phrase # Safety - `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError
Validate a mnemonic phrase in any supported BIP-39 language. Shares the parse path with `mnemonic_to_seed` and `wallet_create_from_mnemonic`, so a phrase this accepts is guaranteed to be accepted by those functions as well. # Safety - `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError

**Safety:**
- `mnemonic` must be a valid null-terminated C string or null - `error` must be a valid pointer to an FFIError
Expand Down
25 changes: 19 additions & 6 deletions key-wallet-ffi/src/account_derivation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::keys::{FFIExtendedPrivKey, FFIPrivateKey};
use crate::{check_ptr, deref_ptr, unwrap_or_return};
use key_wallet::account::derivation::AccountDerivation;
use key_wallet::account::AccountTrait;
use key_wallet::Mnemonic;
use std::ffi::CString;
use std::os::raw::{c_char, c_uint};
use std::ptr;
Expand Down Expand Up @@ -97,7 +98,7 @@ pub unsafe extern "C" fn bls_account_derive_private_key_from_seed(
/// it with `string_free`.
///
/// Notes:
/// - Uses the English wordlist for parsing the mnemonic.
/// - Accepts a mnemonic in any supported BIP-39 language (detected automatically).
/// - Chain-agnostic; may return an error for accounts with internal/external chains.
///
/// # Safety
Expand All @@ -123,11 +124,13 @@ pub unsafe extern "C" fn bls_account_derive_private_key_from_mnemonic(
} else {
Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error))
};
let mnemonic_lang =
unwrap_or_return!(Mnemonic::from_phrase_in_any_language(mnemonic_str), error).language();
let sk = unwrap_or_return!(
account.inner().derive_from_mnemonic_private_key_at(
mnemonic_str,
passphrase_str,
key_wallet::mnemonic::Language::English,
mnemonic_lang,
index,
),
error
Expand Down Expand Up @@ -175,7 +178,7 @@ pub unsafe extern "C" fn eddsa_account_derive_private_key_from_seed(
/// it with `string_free`.
///
/// Notes:
/// - Uses the English wordlist for parsing the mnemonic.
/// - Accepts a mnemonic in any supported BIP-39 language (detected automatically).
///
/// # Safety
/// - `account` must be a valid, non-null pointer to an `FFIEdDSAAccount` (only when `eddsa` feature is enabled).
Expand All @@ -200,11 +203,13 @@ pub unsafe extern "C" fn eddsa_account_derive_private_key_from_mnemonic(
} else {
Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error))
};
let mnemonic_lang =
unwrap_or_return!(Mnemonic::from_phrase_in_any_language(mnemonic_str), error).language();
let sk = unwrap_or_return!(
account.inner().derive_from_mnemonic_private_key_at(
mnemonic_str,
passphrase_str,
key_wallet::mnemonic::Language::English,
mnemonic_lang,
index,
),
error
Expand Down Expand Up @@ -332,6 +337,8 @@ pub unsafe extern "C" fn account_derive_private_key_from_seed(
/// Derive an extended private key from a mnemonic + optional passphrase at the given index.
/// Returns an opaque FFIExtendedPrivKey pointer that must be freed with `extended_private_key_free`.
///
/// Accepts a mnemonic in any supported BIP-39 language (detected automatically).
///
/// # Safety
/// - `account` must be a valid pointer to an FFIAccount
/// - `mnemonic` must be a valid, null-terminated C string
Expand All @@ -353,11 +360,13 @@ pub unsafe extern "C" fn account_derive_extended_private_key_from_mnemonic(
} else {
Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error))
};
let mnemonic_lang =
unwrap_or_return!(Mnemonic::from_phrase_in_any_language(mnemonic_str), error).language();
let derived = unwrap_or_return!(
account.inner().derive_from_mnemonic_extended_xpriv_at(
mnemonic_str,
passphrase_str,
key_wallet::mnemonic::Language::English,
mnemonic_lang,
index,
),
error
Expand All @@ -368,6 +377,8 @@ pub unsafe extern "C" fn account_derive_extended_private_key_from_mnemonic(
/// Derive a private key from a mnemonic + optional passphrase at the given index.
/// Returns an opaque FFIPrivateKey pointer that must be freed with `private_key_free`.
///
/// Accepts a mnemonic in any supported BIP-39 language (detected automatically).
///
/// # Safety
/// - `account` must be a valid pointer to an FFIAccount
/// - `mnemonic` must be a valid, null-terminated C string
Expand All @@ -389,11 +400,13 @@ pub unsafe extern "C" fn account_derive_private_key_from_mnemonic(
} else {
Some(unwrap_or_return!(std::ffi::CStr::from_ptr(passphrase).to_str(), error))
};
let mnemonic_lang =
unwrap_or_return!(Mnemonic::from_phrase_in_any_language(mnemonic_str), error).language();
let derived = unwrap_or_return!(
account.inner().derive_from_mnemonic_extended_xpriv_at(
mnemonic_str,
passphrase_str,
key_wallet::mnemonic::Language::English,
mnemonic_lang,
index,
),
error
Expand Down
155 changes: 155 additions & 0 deletions key-wallet-ffi/src/account_derivation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,159 @@ mod tests {
wallet::wallet_free(wallet);
}
}

/// French mnemonic for entropy 0c1e24e5917779d297e14d45f14e1a1a — contains
/// non-ASCII words, so an English-only parse rejects it.
const FRENCH_MNEMONIC: &str = "amour troupeau couteau brèche gustatif tenaille exécuter capuche dicter lagune jaune cogner";

/// The `*_derive_*_from_mnemonic` helpers must auto-detect the phrase's
/// language: for a French mnemonic they must derive the same keys as their
/// `*_from_seed` counterparts fed the seed of that same phrase.
#[test]
#[cfg(all(feature = "bls", feature = "eddsa"))]
fn test_provider_key_derivation_from_non_english_mnemonic() {
let mut error = FFIError::default();

let mnemonic = std::ffi::CString::new(FRENCH_MNEMONIC).unwrap();
let passphrase = std::ffi::CString::new("").unwrap();

let wallet = unsafe {
wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Mainnet, &mut error)
};
assert!(!wallet.is_null(), "French mnemonic must create a wallet");

let mut seed = [0u8; 64];
let ok = unsafe {
crate::mnemonic::mnemonic_to_seed(
mnemonic.as_ptr(),
passphrase.as_ptr(),
seed.as_mut_ptr(),
&mut (seed.len()),
&mut error,
)
};
assert!(ok, "French mnemonic must derive a seed");

let collection =
unsafe { crate::account_collection::wallet_get_account_collection(wallet, &mut error) };
assert!(!collection.is_null());

unsafe {
// BLS operator key 0: mnemonic-based and seed-based derivation agree.
let operator_account =
crate::account_collection::account_collection_get_provider_operator_keys(collection)
as *mut crate::account::FFIBLSAccount;
assert!(!operator_account.is_null());

let sk_seed = super::super::bls_account_derive_private_key_from_seed(
operator_account,
seed.as_ptr(),
seed.len(),
0,
&mut error,
);
assert!(!sk_seed.is_null(), "BLS from-seed failed: {:?}", error.code);
let sk_mnemonic = super::super::bls_account_derive_private_key_from_mnemonic(
operator_account,
mnemonic.as_ptr(),
passphrase.as_ptr(),
0,
&mut error,
);
assert!(!sk_mnemonic.is_null(), "BLS from-mnemonic failed: {:?}", error.code);
assert_eq!(
std::ffi::CStr::from_ptr(sk_seed).to_str().unwrap(),
std::ffi::CStr::from_ptr(sk_mnemonic).to_str().unwrap(),
"BLS keys from mnemonic and from its seed must match"
);
crate::utils::string_free(sk_seed);
crate::utils::string_free(sk_mnemonic);
crate::account::bls_account_free(operator_account);

// Ed25519 platform node key 0: same agreement.
let platform_account =
crate::account_collection::account_collection_get_provider_platform_keys(collection)
as *mut crate::account::FFIEdDSAAccount;
assert!(!platform_account.is_null());

let node_seed = super::super::eddsa_account_derive_private_key_from_seed(
platform_account,
seed.as_ptr(),
seed.len(),
0,
&mut error,
);
assert!(!node_seed.is_null(), "Ed25519 from-seed failed: {:?}", error.code);
let node_mnemonic = super::super::eddsa_account_derive_private_key_from_mnemonic(
platform_account,
mnemonic.as_ptr(),
passphrase.as_ptr(),
0,
&mut error,
);
assert!(!node_mnemonic.is_null(), "Ed25519 from-mnemonic failed: {:?}", error.code);
assert_eq!(
std::ffi::CStr::from_ptr(node_seed).to_str().unwrap(),
std::ffi::CStr::from_ptr(node_mnemonic).to_str().unwrap(),
"Ed25519 keys from mnemonic and from its seed must match"
);
crate::utils::string_free(node_seed);
crate::utils::string_free(node_mnemonic);
crate::account::eddsa_account_free(platform_account);

crate::account_collection::account_collection_free(collection);
wallet::wallet_free(wallet);
}
}

/// For standard (internal/external-chain) accounts the secp helpers still
/// refuse — but with the chain error, not a language error: the French
/// phrase must get past the mnemonic parse.
#[test]
fn test_secp_mnemonic_helpers_parse_non_english() {
let mut error = FFIError::default();

let mnemonic = std::ffi::CString::new(FRENCH_MNEMONIC).unwrap();
let passphrase = std::ffi::CString::new("").unwrap();

let wallet = unsafe {
wallet::wallet_create_from_mnemonic(mnemonic.as_ptr(), FFINetwork::Testnet, &mut error)
};
assert!(!wallet.is_null());
let account = unsafe {
crate::account::wallet_get_account(wallet, 0, FFIAccountKind::StandardBIP44).account
};
assert!(!account.is_null());

let xpriv = unsafe {
super::super::account_derive_extended_private_key_from_mnemonic(
account,
mnemonic.as_ptr(),
passphrase.as_ptr(),
0,
&mut error,
)
};
assert!(xpriv.is_null());
// InvalidInput = the standard-account chain refusal. Before the fix
// this was InvalidMnemonic — the parse itself rejected the phrase.
assert_eq!(error.code, FFIErrorCode::InvalidInput);

let priv_key = unsafe {
super::super::account_derive_private_key_from_mnemonic(
account,
mnemonic.as_ptr(),
passphrase.as_ptr(),
0,
&mut error,
)
};
assert!(priv_key.is_null());
assert_eq!(error.code, FFIErrorCode::InvalidInput);

unsafe {
account_free(account);
wallet::wallet_free(wallet);
}
}
}
Loading
Loading