Merge pull request #518 from logos-blockchain/Pravdyvy/ffi-mnemonic

feat(wallet-ffi)!: mnemonic-based updates
This commit is contained in:
Pravdyvy 2026-06-24 17:11:08 +03:00 committed by GitHub
commit fb8cbac40e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 2208 additions and 639 deletions

View File

@ -18,7 +18,6 @@ ignore = [
{ id = "RUSTSEC-2024-0370", reason = "transitive dependency of `logos-blockchain-http-api-common`, can't do anything than wait for upstream fix" },
{ id = "RUSTSEC-2026-0173", reason = "`proc-macro-error2` is unmaintained; pulled in transitively via `leptos_macro` and `overwatch-derive`, waiting on upstream fix" },
{ id = "RUSTSEC-2026-0185", reason = "`quinn-proto` remote memory exhaustion; pulled in transitively via `logos-blockchain-libp2p`, waiting on upstream fix" },
]
yanked = "deny"
unused-ignored-advisory = "deny"

7
Cargo.lock generated
View File

@ -2038,7 +2038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
dependencies = [
"data-encoding",
"syn 2.0.117",
"syn 1.0.109",
]
[[package]]
@ -7610,9 +7610,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.11.14"
version = "0.11.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
dependencies = [
"bytes",
"getrandom 0.3.4",
@ -10765,6 +10765,7 @@ dependencies = [
name = "wallet-ffi"
version = "0.1.0"
dependencies = [
"bip39",
"cbindgen",
"common",
"key_protocol",

2222
flake.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -33,6 +33,7 @@ use wallet_ffi::{
FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys,
FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error,
generic_transaction::{FfiProgramWithDependencies, FfiTransactionResult},
wallet::FfiCreateWalletOutput,
};
unsafe extern "C" {
@ -40,7 +41,7 @@ unsafe extern "C" {
config_path: *const c_char,
storage_path: *const c_char,
password: *const c_char,
) -> *mut WalletHandle;
) -> FfiCreateWalletOutput;
fn wallet_ffi_open(
config_path: *const c_char,
@ -214,6 +215,13 @@ unsafe extern "C" {
out_block_height: *mut u64,
) -> error::WalletFfiError;
fn wallet_ffi_restore_data(
handle: *mut WalletHandle,
mnemonic: *const c_char,
password: *const c_char,
depth: u32,
) -> error::WalletFfiError;
fn wallet_ffi_resolve_public_account(
account_id: FfiBytes32,
needs_sign: bool,
@ -254,7 +262,7 @@ unsafe extern "C" {
fn new_wallet_ffi_with_test_context_config(
ctx: &BlockingTestContext,
home: &Path,
) -> Result<*mut WalletHandle> {
) -> Result<FfiCreateWalletOutput> {
let config_path = home.join("wallet_config.json");
let storage_path = home.join("storage.json");
let mut config = ctx.ctx().wallet().config().to_owned();
@ -275,7 +283,7 @@ fn new_wallet_ffi_with_test_context_config(
let storage_path = CString::new(storage_path.to_str().unwrap())?;
let password = CString::new(ctx.ctx().wallet_password())?;
let wallet_ffi_handle = unsafe {
let create_wallet_result = unsafe {
wallet_ffi_create_new(
config_path.as_ptr(),
storage_path.as_ptr(),
@ -293,8 +301,10 @@ fn new_wallet_ffi_with_test_context_config(
.unwrap()
.to_string();
let private_key_hex = CString::new(private_key_hex)?;
unsafe { wallet_ffi_import_public_account(wallet_ffi_handle, private_key_hex.as_ptr()) }
.unwrap();
unsafe {
wallet_ffi_import_public_account(create_wallet_result.wallet, private_key_hex.as_ptr())
}
.unwrap();
}
for (account_id, _chain_index) in source_key_chain.private_account_ids() {
@ -317,7 +327,7 @@ fn new_wallet_ffi_with_test_context_config(
unsafe {
wallet_ffi_import_private_account(
wallet_ffi_handle,
create_wallet_result.wallet,
key_chain_json.as_ptr(),
chain_index_ptr,
&raw const identifier,
@ -327,10 +337,10 @@ fn new_wallet_ffi_with_test_context_config(
.unwrap();
}
Ok(wallet_ffi_handle)
Ok(create_wallet_result)
}
fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandle> {
fn new_wallet_ffi_with_default_config(password: &str) -> Result<FfiCreateWalletOutput> {
let tempdir = tempdir()?;
let config_path = tempdir.path().join("wallet_config.json");
let storage_path = tempdir.path().join("storage.json");
@ -338,13 +348,15 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl
let storage_path_c = CString::new(storage_path.to_str().unwrap())?;
let password = CString::new(password)?;
Ok(unsafe {
let create_wallet_result = unsafe {
wallet_ffi_create_new(
config_path_c.as_ptr(),
storage_path_c.as_ptr(),
password.as_ptr(),
)
})
};
Ok(create_wallet_result)
}
fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
@ -365,7 +377,10 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
let new_public_account_ids_ffi = unsafe {
let mut account_ids = Vec::new();
let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_default_config(password)?;
for _ in 0..n_accounts {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut out_account_id).unwrap();
@ -401,7 +416,10 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
let new_npks_ffi = unsafe {
let mut npks = Vec::new();
let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_default_config(password)?;
for _ in 0..n_accounts {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
@ -430,7 +448,10 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
let home = tempfile::tempdir()?;
// Create a receiving key and save
let first_npk = unsafe {
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let npk = out_keys.nullifier_public_key.data;
@ -467,7 +488,10 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
// Create the wallet FFI and track which account IDs were created as public/private
let (wallet_ffi_handle, created_public_ids) = unsafe {
let handle = new_wallet_ffi_with_default_config(password)?;
let FfiCreateWalletOutput {
wallet: handle,
mnemonic: _,
} = new_wallet_ffi_with_default_config(password)?;
let mut public_ids: Vec<[u8; 32]> = Vec::new();
// Create 5 public accounts and 5 receiving keys
@ -532,7 +556,10 @@ fn test_wallet_ffi_get_balance_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let account_id: AccountId = ctx.ctx().existing_public_accounts()[0];
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
@ -562,7 +589,10 @@ fn test_wallet_ffi_get_account_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let account_id: AccountId = ctx.ctx().existing_public_accounts()[0];
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let mut out_account = FfiAccount::default();
let account: Account = unsafe {
@ -599,7 +629,10 @@ fn test_wallet_ffi_get_account_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let account_id: AccountId = ctx.ctx().existing_private_accounts()[0];
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let mut out_account = FfiAccount::default();
let account: Account = unsafe {
@ -635,7 +668,10 @@ fn test_wallet_ffi_get_public_account_keys() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let account_id: AccountId = ctx.ctx().existing_public_accounts()[0];
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let mut out_key = FfiPublicAccountKey::default();
let key: PublicKey = unsafe {
@ -674,7 +710,10 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let account_id: AccountId = ctx.ctx().existing_private_accounts()[0];
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let mut keys = FfiPrivateAccountKeys::default();
unsafe {
@ -756,7 +795,10 @@ fn wallet_ffi_base58_to_account_id() -> Result<()> {
fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
// Create a new uninitialized public account
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
@ -819,7 +861,10 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
// Create a new private account
let mut out_account_id = FfiBytes32::default();
@ -876,7 +921,10 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
fn test_wallet_ffi_transfer_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into();
let amount: [u8; 16] = 100_u128.to_le_bytes();
@ -930,7 +978,10 @@ fn test_wallet_ffi_transfer_public() -> Result<()> {
fn test_wallet_ffi_transfer_shielded() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let (to, to_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
@ -1006,7 +1057,10 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let amount: [u8; 16] = 100_u128.to_le_bytes();
@ -1066,7 +1120,10 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
fn test_wallet_ffi_transfer_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
let (to, to_keys) = unsafe {
@ -1138,11 +1195,303 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
Ok(())
}
#[test]
fn restore_keys_from_seed_ffi() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let mnemonic = unsafe { CString::from_raw(mnemonic) };
// Create 2 new private accounts
let (private_account_id_1, private_account_1_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};
let (private_account_id_2, private_account_2_keys) = unsafe {
let mut out_keys = FfiPrivateAccountKeys::default();
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
let to: FfiBytes32 = account_id.into();
(to, out_keys)
};
// Create 2 new public accounts
let mut public_account_id_1 = FfiBytes32::default();
unsafe {
wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut public_account_id_1).unwrap();
}
let mut public_account_id_2 = FfiBytes32::default();
unsafe {
wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut public_account_id_2).unwrap();
}
info!("Accounts created");
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
// Send funds to accounts
let from_private: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
let from_public: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let amount_1: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result_1 = FfiTransferResult::default();
unsafe {
let to_identifier = FfiU128 {
data: 0_u128.to_le_bytes(),
};
wallet_ffi_transfer_private(
wallet_ffi_handle,
&raw const from_private,
&raw const private_account_1_keys,
&raw const to_identifier,
&raw const amount_1,
&raw mut transfer_result_1,
)
.unwrap();
}
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
let amount_2: [u8; 16] = 101_u128.to_le_bytes();
let mut transfer_result_2 = FfiTransferResult::default();
unsafe {
let to_identifier = FfiU128 {
data: 0_u128.to_le_bytes(),
};
wallet_ffi_transfer_private(
wallet_ffi_handle,
&raw const from_private,
&raw const private_account_2_keys,
&raw const to_identifier,
&raw const amount_2,
&raw mut transfer_result_2,
)
.unwrap();
}
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
let amount_3: [u8; 16] = 102_u128.to_le_bytes();
let mut transfer_result_3 = FfiTransferResult::default();
unsafe {
wallet_ffi_transfer_public(
wallet_ffi_handle,
&raw const from_public,
&raw const public_account_id_1,
&raw const amount_3,
&raw mut transfer_result_3,
)
.unwrap();
}
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
let amount_4: [u8; 16] = 103_u128.to_le_bytes();
let mut transfer_result_4 = FfiTransferResult::default();
unsafe {
wallet_ffi_transfer_public(
wallet_ffi_handle,
&raw const from_public,
&raw const public_account_id_2,
&raw const amount_4,
&raw mut transfer_result_4,
)
.unwrap();
}
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
unsafe {
wallet_ffi_free_transfer_result(&raw mut transfer_result_1);
wallet_ffi_free_transfer_result(&raw mut transfer_result_2);
wallet_ffi_free_transfer_result(&raw mut transfer_result_3);
wallet_ffi_free_transfer_result(&raw mut transfer_result_4);
}
info!("Preparation complete, performing keys restoration");
let password = CString::new(ctx.ctx().wallet_password())?;
info!("Checking balance correctness before restoration");
let private_account_id_1_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const private_account_id_1,
false,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
let private_account_id_2_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const private_account_id_2,
false,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
let public_account_id_1_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const public_account_id_1,
true,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
let public_account_id_2_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const public_account_id_2,
true,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
assert_eq!(private_account_id_1_balance, 100);
assert_eq!(private_account_id_2_balance, 101);
assert_eq!(public_account_id_1_balance, 102);
assert_eq!(public_account_id_2_balance, 103);
unsafe {
wallet_ffi_restore_data(wallet_ffi_handle, mnemonic.as_ptr(), password.as_ptr(), 5)
.unwrap();
}
// Sync private account local storage with onchain encrypted state
unsafe {
let mut current_height = 0;
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
info!("Checking balance correctness after restoration");
let private_account_id_1_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const private_account_id_1,
false,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
let private_account_id_2_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const private_account_id_2,
false,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
let public_account_id_1_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const public_account_id_1,
true,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
let public_account_id_2_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const public_account_id_2,
true,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
assert_eq!(private_account_id_1_balance, 100);
assert_eq!(private_account_id_2_balance, 101);
assert_eq!(public_account_id_1_balance, 102);
assert_eq!(public_account_id_2_balance, 103);
info!("Accounts restored");
Ok(())
}
#[test]
fn test_wallet_ffi_bridge_withdraw() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let bridge_account: FfiBytes32 = lee::system_bridge_account_id().into();
let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]);
@ -1202,7 +1551,10 @@ fn test_wallet_ffi_bridge_withdraw() -> Result<()> {
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into();
let amount = 100_u128;
@ -1294,7 +1646,10 @@ fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_private_accounts()[1].into();
let amount = 100_u128;
@ -1400,7 +1755,10 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let sender = ctx.ctx().existing_public_accounts()[0];
let owner = ctx.ctx().existing_public_accounts()[1];
@ -1482,7 +1840,10 @@ fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
mnemonic: _,
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let sender = ctx.ctx().existing_public_accounts()[0];
let owner = ctx.ctx().existing_private_accounts()[0];

View File

@ -21,6 +21,7 @@ tokio.workspace = true
key_protocol.workspace = true
serde_json.workspace = true
risc0-zkvm.workspace = true
bip39.workspace = true
vault_core.workspace = true
[build-dependencies]

View File

@ -1,16 +1,18 @@
//! Wallet lifecycle management functions.
use std::{
ffi::{c_char, CStr},
ffi::{c_char, CStr, CString},
path::PathBuf,
ptr,
str::FromStr as _,
sync::Mutex,
};
use wallet::WalletCore;
use bip39::Mnemonic;
use wallet::{cli::execute_keys_restoration, WalletCore};
use crate::{
c_str_to_string,
block_on, c_str_to_string,
error::{print_error, WalletFfiError},
types::WalletHandle,
};
@ -20,6 +22,22 @@ pub(crate) struct WalletWrapper {
pub core: Mutex<WalletCore>,
}
#[repr(C)]
pub struct FfiCreateWalletOutput {
pub wallet: *mut WalletHandle,
/// C compatible(null terminated) string.
pub mnemonic: *mut c_char,
}
impl Default for FfiCreateWalletOutput {
fn default() -> Self {
Self {
wallet: std::ptr::null_mut(),
mnemonic: std::ptr::null_mut(),
}
}
}
/// Helper to get the wallet wrapper from an opaque handle.
pub(crate) fn get_wallet(
handle: *mut WalletHandle,
@ -71,8 +89,8 @@ fn c_str_to_path(ptr: *const c_char, name: &str) -> Result<PathBuf, WalletFfiErr
/// - `password`: Password for encrypting the wallet seed
///
/// # Returns
/// - Opaque wallet handle on success
/// - Null pointer on error (call `wallet_ffi_get_last_error()` for details)
/// - Result, which contains opaque wallet handle and mnemonic words on success
/// - Result with null pointers on error (call `wallet_ffi_get_last_error()` for details)
///
/// # Safety
/// All string parameters must be valid null-terminated UTF-8 strings.
@ -81,29 +99,40 @@ pub unsafe extern "C" fn wallet_ffi_create_new(
config_path: *const c_char,
storage_path: *const c_char,
password: *const c_char,
) -> *mut WalletHandle {
) -> FfiCreateWalletOutput {
let Ok(config_path) = c_str_to_path(config_path, "config_path") else {
return ptr::null_mut();
return FfiCreateWalletOutput::default();
};
let Ok(storage_path) = c_str_to_path(storage_path, "storage_path") else {
return ptr::null_mut();
return FfiCreateWalletOutput::default();
};
let Ok(password) = c_str_to_string(password, "password") else {
return ptr::null_mut();
return FfiCreateWalletOutput::default();
};
match WalletCore::new_init_storage(config_path, storage_path, None, &password) {
Ok((core, _mnemonic)) => {
Ok((core, mnemonic)) => {
let wrapper = Box::new(WalletWrapper {
core: Mutex::new(core),
});
Box::into_raw(wrapper).cast::<WalletHandle>()
let handle = Box::into_raw(wrapper).cast::<WalletHandle>();
let Ok(c_mnemonic_string) = CString::new(mnemonic.to_string()) else {
return FfiCreateWalletOutput::default();
};
let raw_pointer = CString::into_raw(c_mnemonic_string);
FfiCreateWalletOutput {
wallet: handle,
mnemonic: raw_pointer,
}
}
Err(e) => {
print_error(format!("Failed to create wallet: {e}"));
ptr::null_mut()
FfiCreateWalletOutput::default()
}
}
}
@ -204,6 +233,81 @@ pub unsafe extern "C" fn wallet_ffi_save(handle: *mut WalletHandle) -> WalletFfi
}
}
/// Restore wallet data from mnemonic and password.
///
/// # Parameters
/// - `handle`: Valid wallet handle
/// - `mnemonic`: Valid pointer to instance of `* char`, provided by `wallet_ffi_create_new`
/// - `password`: Valid pointer to C string.
/// - `depth`: Depth of a reconstructed tree
///
/// # Returns
/// - `Success` on successful restoration
/// - Error code on failure
///
/// # Safety
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
/// - `mnemonic` must be a valid pointer to instance of `* char`, provided by
/// `wallet_ffi_create_new`
/// - `password` must be a valid pointer to C string.
/// - `depth` parameter induces exponential growth in execution time, be aware of it.
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_restore_data(
handle: *mut WalletHandle,
mnemonic: *const c_char,
password: *const c_char,
depth: u32,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
Ok(w) => w,
Err(e) => return e,
};
let mut wallet = match wrapper.core.lock() {
Ok(w) => w,
Err(e) => {
print_error(format!("Failed to lock wallet: {e}"));
return WalletFfiError::InternalError;
}
};
let Ok(password) = c_str_to_string(password, "password") else {
return WalletFfiError::NullPointer;
};
let Ok(mnemonic) = c_str_to_string(mnemonic, "mnemonic") else {
return WalletFfiError::NullPointer;
};
let mnemonic = match Mnemonic::from_str(&mnemonic) {
Ok(mn) => mn,
Err(e) => {
print_error(format!("Failed to parse mnemonic: {e}"));
return WalletFfiError::SerializationError;
}
};
let res = match wallet.restore_storage(&mnemonic, &password) {
Ok(()) => WalletFfiError::Success,
Err(e) => {
print_error(format!("Failed to restore wallet data: {e}"));
WalletFfiError::StorageError
}
};
if res == WalletFfiError::Success {
match block_on(execute_keys_restoration(&mut wallet, depth)) {
Ok(()) => WalletFfiError::Success,
Err(err) => {
print_error(format!("Failed to restore wallet data: {err}"));
WalletFfiError::StorageError
}
}
} else {
res
}
}
/// Get the sequencer address from the wallet configuration.
///
/// # Parameters

View File

@ -299,6 +299,14 @@ typedef struct FfiPublicAccountKey {
struct FfiBytes32 public_key;
} FfiPublicAccountKey;
typedef struct FfiCreateWalletOutput {
struct WalletHandle *wallet;
/**
* C compatible(null terminated) string.
*/
char *mnemonic;
} FfiCreateWalletOutput;
/**
* Create a new public account.
*
@ -1452,15 +1460,15 @@ enum WalletFfiError wallet_ffi_vault_claim_private(struct WalletHandle *handle,
* - `password`: Password for encrypting the wallet seed
*
* # Returns
* - Opaque wallet handle on success
* - Null pointer on error (call `wallet_ffi_get_last_error()` for details)
* - Result, which contains opaque wallet handle and mnemonic words on success
* - Result with null pointers on error (call `wallet_ffi_get_last_error()` for details)
*
* # Safety
* All string parameters must be valid null-terminated UTF-8 strings.
*/
struct WalletHandle *wallet_ffi_create_new(const char *config_path,
const char *storage_path,
const char *password);
struct FfiCreateWalletOutput wallet_ffi_create_new(const char *config_path,
const char *storage_path,
const char *password);
/**
* Open an existing wallet from storage.
@ -1510,6 +1518,31 @@ void wallet_ffi_destroy(struct WalletHandle *handle);
*/
enum WalletFfiError wallet_ffi_save(struct WalletHandle *handle);
/**
* Restore wallet data from mnemonic and password.
*
* # Parameters
* - `handle`: Valid wallet handle
* - `mnemonic`: Valid pointer to instance of `* char`, provided by `wallet_ffi_create_new`
* - `password`: Valid pointer to C string.
* - `depth`: Depth of a reconstructed tree
*
* # Returns
* - `Success` on successful restoration
* - Error code on failure
*
* # Safety
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
* - `mnemonic` must be a valid pointer to instance of `* char`, provided by
* `wallet_ffi_create_new`
* - `password` must be a valid pointer to C string.
* - `depth` parameter induces exponential growth in execution time, be aware of it.
*/
enum WalletFfiError wallet_ffi_restore_data(struct WalletHandle *handle,
const char *mnemonic,
const char *password,
uint32_t depth);
/**
* Get the sequencer address from the wallet configuration.
*

View File

@ -872,3 +872,26 @@ impl WalletCore {
&self.config_overrides
}
}
#[cfg(test)]
mod tests {
use std::{ffi::CString, str::FromStr as _};
use bip39::Mnemonic;
#[test]
fn mnemonic_roundtrip() {
let mnemonic =
Mnemonic::from_entropy(&[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).unwrap();
let c_mnemonic_string = CString::new(mnemonic.to_string()).unwrap();
let c_mnemonic_string_raw = c_mnemonic_string.into_raw();
// Safety: Will be safe, pointer is created from CString
let c_str = unsafe { CString::from_raw(c_mnemonic_string_raw) };
let mn_string = c_str.to_str().unwrap();
let mn_ret = Mnemonic::from_str(mn_string).unwrap();
assert_eq!(mnemonic, mn_ret);
}
}

View File

@ -94,6 +94,7 @@ impl Storage {
self.key_chain = UserKeyChain::new_with_accounts(public_tree, private_tree);
self.labels = BTreeMap::new();
self.last_synced_block = 0;
Ok(())
}