diff --git a/Cargo.lock b/Cargo.lock index d6a7b766..72e408c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2919,10 +2919,12 @@ dependencies = [ "nssa_core", "sequencer_core", "sequencer_runner", + "serde_json", "tempfile", "tokio", "url", "wallet", + "wallet-ffi", ] [[package]] @@ -6696,6 +6698,8 @@ dependencies = [ "cbindgen", "common", "nssa", + "nssa_core", + "tempfile", "tokio", "wallet", ] diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 5ddd2cbd..f51486b8 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -14,6 +14,8 @@ common.workspace = true key_protocol.workspace = true indexer_core.workspace = true url.workspace = true +wallet-ffi.workspace = true +serde_json.workspace = true anyhow.workspace = true env_logger.workspace = true diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 21e1ca81..fbdd233f 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -44,6 +44,7 @@ pub struct TestContext { indexer_loop_handle: Option>>, sequencer_client: SequencerClient, wallet: WalletCore, + wallet_password: String, _temp_sequencer_dir: TempDir, _temp_wallet_dir: TempDir, } @@ -114,7 +115,7 @@ impl TestContext { format!("http://{sequencer_addr}") }; - let (wallet, temp_wallet_dir) = Self::setup_wallet(sequencer_addr.clone()) + let (wallet, temp_wallet_dir, wallet_password) = Self::setup_wallet(sequencer_addr.clone()) .await .context("Failed to setup wallet")?; @@ -142,6 +143,7 @@ impl TestContext { wallet, _temp_sequencer_dir: temp_sequencer_dir, _temp_wallet_dir: temp_wallet_dir, + wallet_password, }) } else { Ok(Self { @@ -153,6 +155,7 @@ impl TestContext { wallet, _temp_sequencer_dir: temp_sequencer_dir, _temp_wallet_dir: temp_wallet_dir, + wallet_password, }) } } @@ -193,7 +196,7 @@ impl TestContext { )) } - async fn setup_wallet(sequencer_addr: String) -> Result<(WalletCore, TempDir)> { + async fn setup_wallet(sequencer_addr: String) -> Result<(WalletCore, TempDir, String)> { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let wallet_config_source_path = PathBuf::from(manifest_dir).join("configs/wallet/wallet_config.json"); @@ -211,11 +214,12 @@ impl TestContext { ..Default::default() }; + let wallet_password = "test_pass".to_owned(); let wallet = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), - "test_pass".to_owned(), + wallet_password.clone(), ) .context("Failed to init wallet")?; wallet @@ -223,7 +227,7 @@ impl TestContext { .await .context("Failed to store wallet persistent data")?; - Ok((wallet, temp_wallet_dir)) + Ok((wallet, temp_wallet_dir, wallet_password)) } /// Get reference to the wallet. @@ -231,6 +235,10 @@ impl TestContext { &self.wallet } + pub fn wallet_password(&self) -> &str { + &self.wallet_password + } + /// Get mutable reference to the wallet. pub fn wallet_mut(&mut self) -> &mut WalletCore { &mut self.wallet @@ -255,6 +263,7 @@ impl Drop for TestContext { wallet: _, _temp_sequencer_dir, _temp_wallet_dir, + wallet_password: _, } = self; sequencer_loop_handle.abort(); @@ -268,6 +277,20 @@ impl Drop for TestContext { } } +/// A test context to be used in normal #[test] tests +pub struct BlockingTestContext { + pub ctx: TestContext, + pub runtime: tokio::runtime::Runtime, +} + +impl BlockingTestContext { + pub fn new() -> Result { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let ctx = runtime.block_on(TestContext::new())?; + Ok(Self { ctx, runtime }) + } +} + pub fn format_public_account_id(account_id: &str) -> String { format!("Public/{account_id}") } diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs new file mode 100644 index 00000000..bb6b9805 --- /dev/null +++ b/integration_tests/tests/wallet_ffi.rs @@ -0,0 +1,618 @@ +use std::{ + collections::HashSet, + ffi::{CStr, CString, c_char}, + io::Write, + time::Duration, +}; + +use anyhow::Result; +use integration_tests::{ + ACC_RECEIVER, ACC_SENDER, ACC_SENDER_PRIVATE, BlockingTestContext, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, +}; +use log::info; +use nssa::{Account, AccountId, PublicKey, program::Program}; +use nssa_core::program::DEFAULT_PROGRAM_ID; +use tempfile::tempdir; +use wallet::WalletCore; +use wallet_ffi::{ + FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, + FfiTransferResult, WalletHandle, error, +}; + +unsafe extern "C" { + fn wallet_ffi_create_new( + config_path: *const c_char, + storage_path: *const c_char, + password: *const c_char, + ) -> *mut WalletHandle; + + fn wallet_ffi_destroy(handle: *mut WalletHandle); + + fn wallet_ffi_create_account_public( + handle: *mut WalletHandle, + out_account_id: *mut FfiBytes32, + ) -> error::WalletFfiError; + + fn wallet_ffi_create_account_private( + handle: *mut WalletHandle, + out_account_id: *mut FfiBytes32, + ) -> error::WalletFfiError; + + fn wallet_ffi_list_accounts( + handle: *mut WalletHandle, + out_list: *mut FfiAccountList, + ) -> error::WalletFfiError; + + fn wallet_ffi_free_account_list(list: *mut FfiAccountList); + + fn wallet_ffi_get_balance( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + is_public: bool, + out_balance: *mut [u8; 16], + ) -> error::WalletFfiError; + + fn wallet_ffi_get_account_public( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_account: *mut FfiAccount, + ) -> error::WalletFfiError; + + fn wallet_ffi_free_account_data(account: *mut FfiAccount); + + fn wallet_ffi_get_public_account_key( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_public_key: *mut FfiPublicAccountKey, + ) -> error::WalletFfiError; + + fn wallet_ffi_get_private_account_keys( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_keys: *mut FfiPrivateAccountKeys, + ) -> error::WalletFfiError; + + fn wallet_ffi_free_private_account_keys(keys: *mut FfiPrivateAccountKeys); + + fn wallet_ffi_account_id_to_base58(account_id: *const FfiBytes32) -> *mut std::ffi::c_char; + + fn wallet_ffi_free_string(ptr: *mut c_char); + + fn wallet_ffi_account_id_from_base58( + base58_str: *const std::ffi::c_char, + out_account_id: *mut FfiBytes32, + ) -> error::WalletFfiError; + + fn wallet_ffi_transfer_public( + handle: *mut WalletHandle, + from: *const FfiBytes32, + to: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + + fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult); + + fn wallet_ffi_register_public_account( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; +} + +fn new_wallet_ffi_with_test_context_config(ctx: &BlockingTestContext) -> *mut WalletHandle { + let tempdir = tempfile::tempdir().unwrap(); + let config_path = tempdir.path().join("wallet_config.json"); + let storage_path = tempdir.path().join("storage.json"); + let mut config = ctx.ctx.wallet().config().to_owned(); + if let Some(config_overrides) = ctx.ctx.wallet().config_overrides().clone() { + config.apply_overrides(config_overrides); + } + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&config_path) + .unwrap(); + + let config_with_overrides_serialized = serde_json::to_vec_pretty(&config).unwrap(); + + file.write_all(&config_with_overrides_serialized).unwrap(); + + let config_path = CString::new(config_path.to_str().unwrap()).unwrap(); + let storage_path = CString::new(storage_path.to_str().unwrap()).unwrap(); + let password = CString::new(ctx.ctx.wallet_password()).unwrap(); + + unsafe { + wallet_ffi_create_new( + config_path.as_ptr(), + storage_path.as_ptr(), + password.as_ptr(), + ) + } +} + +fn new_wallet_ffi_with_default_config(password: &str) -> *mut WalletHandle { + let tempdir = tempdir().unwrap(); + let config_path = tempdir.path().join("wallet_config.json"); + let storage_path = tempdir.path().join("storage.json"); + let config_path_c = CString::new(config_path.to_str().unwrap()).unwrap(); + let storage_path_c = CString::new(storage_path.to_str().unwrap()).unwrap(); + let password = CString::new(password).unwrap(); + + unsafe { + wallet_ffi_create_new( + config_path_c.as_ptr(), + storage_path_c.as_ptr(), + password.as_ptr(), + ) + } +} + +fn new_wallet_rust_with_default_config(password: &str) -> WalletCore { + let tempdir = tempdir().unwrap(); + let config_path = tempdir.path().join("wallet_config.json"); + let storage_path = tempdir.path().join("storage.json"); + + WalletCore::new_init_storage( + config_path.to_path_buf(), + storage_path.to_path_buf(), + None, + password.to_string(), + ) + .unwrap() +} + +#[test] +fn test_wallet_ffi_create_public_accounts() { + let password = "password_for_tests"; + let n_accounts = 10; + // First `n_accounts` public accounts created with Rust wallet + let new_public_account_ids_rust = { + let mut account_ids = Vec::new(); + + let mut wallet_rust = new_wallet_rust_with_default_config(password); + for _ in 0..n_accounts { + let account_id = wallet_rust.create_new_account_public(None).0; + account_ids.push(*account_id.value()); + } + account_ids + }; + + // First `n_accounts` public accounts created with wallet FFI + let new_public_account_ids_ffi = unsafe { + let mut account_ids = Vec::new(); + + let wallet_ffi_handle = 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, + (&mut out_account_id) as *mut FfiBytes32, + ); + account_ids.push(out_account_id.data); + } + wallet_ffi_destroy(wallet_ffi_handle); + account_ids + }; + + assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust); +} + +#[test] +fn test_wallet_ffi_create_private_accounts() { + let password = "password_for_tests"; + let n_accounts = 10; + // First `n_accounts` private accounts created with Rust wallet + let new_private_account_ids_rust = { + let mut account_ids = Vec::new(); + + let mut wallet_rust = new_wallet_rust_with_default_config(password); + for _ in 0..n_accounts { + let account_id = wallet_rust.create_new_account_private(None).0; + account_ids.push(*account_id.value()); + } + account_ids + }; + + // First `n_accounts` private accounts created with wallet FFI + let new_private_account_ids_ffi = unsafe { + let mut account_ids = Vec::new(); + + let wallet_ffi_handle = 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_private( + wallet_ffi_handle, + (&mut out_account_id) as *mut FfiBytes32, + ); + account_ids.push(out_account_id.data); + } + wallet_ffi_destroy(wallet_ffi_handle); + account_ids + }; + + assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust) +} + +#[test] +fn test_wallet_ffi_list_accounts() { + let password = "password_for_tests"; + + // Create the wallet FFI + let wallet_ffi_handle = unsafe { + let handle = new_wallet_ffi_with_default_config(password); + // Create 5 public accounts and 5 private accounts + for _ in 0..5 { + let mut out_account_id = FfiBytes32::from_bytes([0; 32]); + wallet_ffi_create_account_public(handle, (&mut out_account_id) as *mut FfiBytes32); + wallet_ffi_create_account_private(handle, (&mut out_account_id) as *mut FfiBytes32); + } + + handle + }; + + // Create the wallet Rust + let wallet_rust = { + let mut wallet = new_wallet_rust_with_default_config(password); + // Create 5 public accounts and 5 private accounts + for _ in 0..5 { + wallet.create_new_account_public(None); + wallet.create_new_account_private(None); + } + wallet + }; + + // Get the account list with FFI method + let mut wallet_ffi_account_list = unsafe { + let mut out_list = FfiAccountList::default(); + wallet_ffi_list_accounts(wallet_ffi_handle, (&mut out_list) as *mut FfiAccountList); + out_list + }; + + let wallet_rust_account_ids = wallet_rust + .storage() + .user_data + .account_ids() + .collect::>(); + + // Assert same number of elements between Rust and FFI result + assert_eq!(wallet_rust_account_ids.len(), wallet_ffi_account_list.count); + + let wallet_ffi_account_list_slice = unsafe { + core::slice::from_raw_parts( + wallet_ffi_account_list.entries, + wallet_ffi_account_list.count, + ) + }; + + // Assert same account ids between Rust and FFI result + assert_eq!( + wallet_rust_account_ids + .iter() + .map(|id| id.value()) + .collect::>(), + wallet_ffi_account_list_slice + .iter() + .map(|entry| &entry.account_id.data) + .collect::>() + ); + + // Assert `is_pub` flag is correct in the FFI result + for entry in wallet_ffi_account_list_slice.iter() { + let account_id = AccountId::new(entry.account_id.data); + let is_pub_default_in_rust_wallet = wallet_rust + .storage() + .user_data + .default_pub_account_signing_keys + .contains_key(&account_id); + let is_pub_key_tree_wallet_rust = wallet_rust + .storage() + .user_data + .public_key_tree + .account_id_map + .contains_key(&account_id); + + let is_public_in_rust_wallet = is_pub_default_in_rust_wallet || is_pub_key_tree_wallet_rust; + + assert_eq!(entry.is_public, is_public_in_rust_wallet); + } + + unsafe { + wallet_ffi_free_account_list((&mut wallet_ffi_account_list) as *mut FfiAccountList); + wallet_ffi_destroy(wallet_ffi_handle); + } +} + +#[test] +fn test_wallet_ffi_get_balance_public() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let account_id: AccountId = ACC_SENDER.parse().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx); + + let balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let ffi_account_id = FfiBytes32::from(&account_id); + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + (&ffi_account_id) as *const FfiBytes32, + true, + (&mut out_balance) as *mut [u8; 16], + ); + u128::from_le_bytes(out_balance) + }; + assert_eq!(balance, 10000); + + info!("Successfully retrieved account balance"); + + unsafe { + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_get_account_public() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let account_id: AccountId = ACC_SENDER.parse().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx); + let mut out_account = FfiAccount::default(); + + let account: Account = unsafe { + let ffi_account_id = FfiBytes32::from(&account_id); + let _result = wallet_ffi_get_account_public( + wallet_ffi_handle, + (&ffi_account_id) as *const FfiBytes32, + (&mut out_account) as *mut FfiAccount, + ); + (&out_account).try_into().unwrap() + }; + + assert_eq!( + account.program_owner, + Program::authenticated_transfer_program().id() + ); + assert_eq!(account.balance, 10000); + assert!(account.data.is_empty()); + assert_eq!(account.nonce, 0); + + unsafe { + wallet_ffi_free_account_data((&mut out_account) as *mut FfiAccount); + wallet_ffi_destroy(wallet_ffi_handle); + } + + info!("Successfully retrieved account with correct details"); + + Ok(()) +} + +#[test] +fn test_wallet_ffi_get_public_account_keys() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let account_id: AccountId = ACC_SENDER.parse().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx); + let mut out_key = FfiPublicAccountKey::default(); + + let key: PublicKey = unsafe { + let ffi_account_id = FfiBytes32::from(&account_id); + let _result = wallet_ffi_get_public_account_key( + wallet_ffi_handle, + (&ffi_account_id) as *const FfiBytes32, + (&mut out_key) as *mut FfiPublicAccountKey, + ); + (&out_key).try_into().unwrap() + }; + + let expected_key = { + let private_key = ctx + .ctx + .wallet() + .get_account_public_signing_key(&account_id) + .unwrap(); + PublicKey::new_from_private_key(private_key) + }; + + assert_eq!(key, expected_key); + + info!("Successfully retrieved account key"); + + unsafe { + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_get_private_account_keys() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let account_id: AccountId = ACC_SENDER_PRIVATE.parse().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx); + let mut keys = FfiPrivateAccountKeys::default(); + + unsafe { + let ffi_account_id = FfiBytes32::from(&account_id); + let _result = wallet_ffi_get_private_account_keys( + wallet_ffi_handle, + (&ffi_account_id) as *const FfiBytes32, + (&mut keys) as *mut FfiPrivateAccountKeys, + ); + }; + + let key_chain = &ctx + .ctx + .wallet() + .storage() + .user_data + .get_private_account(&account_id) + .unwrap() + .0; + + let expected_npk = &key_chain.nullifer_public_key; + let expected_ivk = &key_chain.incoming_viewing_public_key; + + assert_eq!(&keys.npk(), expected_npk); + assert_eq!(&keys.ivk().unwrap(), expected_ivk); + + unsafe { + wallet_ffi_free_private_account_keys((&mut keys) as *mut FfiPrivateAccountKeys); + wallet_ffi_destroy(wallet_ffi_handle); + } + + info!("Successfully retrieved account keys"); + + Ok(()) +} + +#[test] +fn test_wallet_ffi_account_id_to_base58() { + let account_id_str = ACC_SENDER; + let account_id: AccountId = account_id_str.parse().unwrap(); + let ffi_bytes: FfiBytes32 = (&account_id).into(); + let ptr = unsafe { wallet_ffi_account_id_to_base58((&ffi_bytes) as *const FfiBytes32) }; + + let ffi_result = unsafe { CStr::from_ptr(ptr).to_str().unwrap() }; + + assert_eq!(account_id_str, ffi_result); + + unsafe { + wallet_ffi_free_string(ptr); + } +} + +#[test] +fn test_wallet_ffi_base58_to_account_id() { + let account_id_str = ACC_SENDER; + let account_id_c_str = CString::new(account_id_str).unwrap(); + let account_id: AccountId = unsafe { + let mut out_account_id_bytes = FfiBytes32::default(); + wallet_ffi_account_id_from_base58( + account_id_c_str.as_ptr(), + (&mut out_account_id_bytes) as *mut FfiBytes32, + ); + out_account_id_bytes.into() + }; + + let expected_account_id = account_id_str.parse().unwrap(); + + assert_eq!(account_id, expected_account_id); +} + +#[test] +fn test_wallet_ffi_init_public_account_auth_transfer() -> Result<()> { + let ctx = BlockingTestContext::new().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx); + + // Create a new uninitialized public account + let mut out_account_id = FfiBytes32::from_bytes([0; 32]); + unsafe { + wallet_ffi_create_account_public( + wallet_ffi_handle, + (&mut out_account_id) as *mut FfiBytes32, + ); + } + + // Check its program owner is the default program id + let account: Account = unsafe { + let mut out_account = FfiAccount::default(); + let _result = wallet_ffi_get_account_public( + wallet_ffi_handle, + (&out_account_id) as *const FfiBytes32, + (&mut out_account) as *mut FfiAccount, + ); + (&out_account).try_into().unwrap() + }; + assert_eq!(account.program_owner, DEFAULT_PROGRAM_ID); + + // Call the init funciton + let mut transfer_result = FfiTransferResult::default(); + unsafe { + wallet_ffi_register_public_account( + wallet_ffi_handle, + (&out_account_id) as *const FfiBytes32, + (&mut transfer_result) as *mut FfiTransferResult, + ); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Check that the program owner is now the authenticated transfer program + let account: Account = unsafe { + let mut out_account = FfiAccount::default(); + let _result = wallet_ffi_get_account_public( + wallet_ffi_handle, + (&out_account_id) as *const FfiBytes32, + (&mut out_account) as *mut FfiAccount, + ); + (&out_account).try_into().unwrap() + }; + assert_eq!( + account.program_owner, + Program::authenticated_transfer_program().id() + ); + + unsafe { + wallet_ffi_free_transfer_result((&mut transfer_result) as *mut FfiTransferResult); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_transfer_public() -> Result<()> { + let ctx = BlockingTestContext::new().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx); + let from: FfiBytes32 = (&ACC_SENDER.parse::().unwrap()).into(); + let to: FfiBytes32 = (&ACC_RECEIVER.parse::().unwrap()).into(); + let amount: [u8; 16] = 100u128.to_le_bytes(); + + let mut transfer_result = FfiTransferResult::default(); + unsafe { + wallet_ffi_transfer_public( + wallet_ffi_handle, + (&from) as *const FfiBytes32, + (&to) as *const FfiBytes32, + (&amount) as *const [u8; 16], + (&mut transfer_result) as *mut FfiTransferResult, + ); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let from_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + (&from) as *const FfiBytes32, + true, + (&mut out_balance) as *mut [u8; 16], + ); + u128::from_le_bytes(out_balance) + }; + + let to_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + (&to) as *const FfiBytes32, + true, + (&mut out_balance) as *mut [u8; 16], + ); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(from_balance, 9900); + assert_eq!(to_balance, 20100); + + unsafe { + wallet_ffi_free_transfer_result((&mut transfer_result) as *mut FfiTransferResult); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index de4b65b2..47a0eadb 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -14,7 +14,7 @@ mod state; pub use nssa_core::{ SharedSecretKey, - account::{Account, AccountId}, + account::{Account, AccountId, Data}, encryption::EphemeralPublicKey, program::ProgramId, }; diff --git a/wallet-ffi/Cargo.toml b/wallet-ffi/Cargo.toml index fd7a1f2a..4305226b 100644 --- a/wallet-ffi/Cargo.toml +++ b/wallet-ffi/Cargo.toml @@ -5,13 +5,17 @@ edition = "2021" license = { workspace = true } [lib] -crate-type = ["cdylib", "staticlib"] +crate-type = ["rlib", "cdylib", "staticlib"] [dependencies] wallet.workspace = true nssa.workspace = true common.workspace = true +nssa_core.workspace = true tokio.workspace = true [build-dependencies] cbindgen = "0.29" + +[dev-dependencies] +tempfile = "3" diff --git a/wallet-ffi/src/account.rs b/wallet-ffi/src/account.rs index b99d10cf..cf237276 100644 --- a/wallet-ffi/src/account.rs +++ b/wallet-ffi/src/account.rs @@ -7,9 +7,7 @@ use nssa::AccountId; use crate::{ block_on, error::{print_error, WalletFfiError}, - types::{ - FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, FfiProgramId, WalletHandle, - }, + types::{FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, WalletHandle}, wallet::get_wallet, }; @@ -349,26 +347,8 @@ pub unsafe extern "C" fn wallet_ffi_get_account_public( Err(e) => return e, }; - // Convert account data to FFI type - let data_vec: Vec = account.data.into(); - let data_len = data_vec.len(); - let data_ptr = if data_len > 0 { - let data_boxed = data_vec.into_boxed_slice(); - Box::into_raw(data_boxed) as *const u8 - } else { - ptr::null() - }; - - let program_owner = FfiProgramId { - data: account.program_owner, - }; - unsafe { - (*out_account).program_owner = program_owner; - (*out_account).balance = account.balance.to_le_bytes(); - (*out_account).nonce = account.nonce.to_le_bytes(); - (*out_account).data = data_ptr; - (*out_account).data_len = data_len; + *out_account = account.into(); } WalletFfiError::Success diff --git a/wallet-ffi/src/error.rs b/wallet-ffi/src/error.rs index ea366475..ab9ce6dd 100644 --- a/wallet-ffi/src/error.rs +++ b/wallet-ffi/src/error.rs @@ -36,6 +36,10 @@ pub enum WalletFfiError { SyncError = 13, /// Serialization/deserialization error SerializationError = 14, + /// Invalid conversion from FFI types to NSSA types + InvalidTypeConversion = 15, + /// Invalid Key value + InvalidKeyValue = 16, /// Internal error (catch-all) InternalError = 99, } diff --git a/wallet-ffi/src/keys.rs b/wallet-ffi/src/keys.rs index e8309a81..08661a50 100644 --- a/wallet-ffi/src/keys.rs +++ b/wallet-ffi/src/keys.rs @@ -65,7 +65,7 @@ pub unsafe extern "C" fn wallet_ffi_get_public_account_key( let public_key = PublicKey::new_from_private_key(private_key); unsafe { - (*out_public_key).public_key.data = *public_key.value(); + *out_public_key = public_key.into(); } WalletFfiError::Success diff --git a/wallet-ffi/src/types.rs b/wallet-ffi/src/types.rs index 3bcfd9fd..a1d70181 100644 --- a/wallet-ffi/src/types.rs +++ b/wallet-ffi/src/types.rs @@ -1,6 +1,12 @@ //! C-compatible type definitions for the FFI layer. -use std::ffi::c_char; +use core::slice; +use std::{ffi::c_char, ptr}; + +use nssa::{Account, Data}; +use nssa_core::encryption::shared_key_derivation::Secp256k1Point; + +use crate::error::WalletFfiError; /// Opaque pointer to the Wallet instance. /// @@ -25,6 +31,13 @@ pub struct FfiProgramId { pub data: [u32; 8], } +/// U128 - 16 bytes little endian +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiU128 { + pub data: [u8; 16], +} + /// Account data structure - C-compatible version of nssa Account. /// /// Note: `balance` and `nonce` are u128 values represented as little-endian @@ -33,23 +46,23 @@ pub struct FfiProgramId { pub struct FfiAccount { pub program_owner: FfiProgramId, /// Balance as little-endian [u8; 16] - pub balance: [u8; 16], + pub balance: FfiU128, /// Pointer to account data bytes pub data: *const u8, /// Length of account data pub data_len: usize, /// Nonce as little-endian [u8; 16] - pub nonce: [u8; 16], + pub nonce: FfiU128, } impl Default for FfiAccount { fn default() -> Self { Self { program_owner: FfiProgramId::default(), - balance: [0u8; 16], + balance: FfiU128::default(), data: std::ptr::null(), data_len: 0, - nonce: [0u8; 16], + nonce: FfiU128::default(), } } } @@ -138,6 +151,40 @@ impl FfiBytes32 { } } +impl FfiPrivateAccountKeys { + pub fn npk(&self) -> nssa_core::NullifierPublicKey { + nssa_core::NullifierPublicKey(self.nullifier_public_key.data) + } + + pub fn ivk(&self) -> Result { + if self.incoming_viewing_public_key_len == 33 { + let slice = unsafe { + slice::from_raw_parts( + self.incoming_viewing_public_key, + self.incoming_viewing_public_key_len, + ) + }; + Ok(Secp256k1Point(slice.to_vec())) + } else { + Err(WalletFfiError::InvalidKeyValue) + } + } +} + +impl From for FfiU128 { + fn from(value: u128) -> Self { + Self { + data: value.to_le_bytes(), + } + } +} + +impl From for u128 { + fn from(value: FfiU128) -> Self { + u128::from_le_bytes(value.data) + } +} + impl From<&nssa::AccountId> for FfiBytes32 { fn from(id: &nssa::AccountId) -> Self { Self::from_account_id(id) @@ -149,3 +196,67 @@ impl From for nssa::AccountId { nssa::AccountId::new(bytes.data) } } + +impl From for FfiAccount { + fn from(value: nssa::Account) -> Self { + // Convert account data to FFI type + let data_vec: Vec = value.data.into(); + let data_len = data_vec.len(); + let data = if data_len > 0 { + let data_boxed = data_vec.into_boxed_slice(); + Box::into_raw(data_boxed) as *const u8 + } else { + ptr::null() + }; + + let program_owner = FfiProgramId { + data: value.program_owner, + }; + FfiAccount { + program_owner, + balance: value.balance.into(), + data, + data_len, + nonce: value.nonce.into(), + } + } +} + +impl TryFrom<&FfiAccount> for nssa::Account { + type Error = WalletFfiError; + + fn try_from(value: &FfiAccount) -> Result { + let data = if value.data_len > 0 { + unsafe { + let slice = slice::from_raw_parts(value.data, value.data_len); + Data::try_from(slice.to_vec()).map_err(|_| WalletFfiError::InvalidTypeConversion)? + } + } else { + Data::default() + }; + Ok(Account { + program_owner: value.program_owner.data, + balance: value.balance.into(), + data, + nonce: value.nonce.into(), + }) + } +} + +impl From for FfiPublicAccountKey { + fn from(value: nssa::PublicKey) -> Self { + Self { + public_key: FfiBytes32::from_bytes(*value.value()), + } + } +} + +impl TryFrom<&FfiPublicAccountKey> for nssa::PublicKey { + type Error = WalletFfiError; + + fn try_from(value: &FfiPublicAccountKey) -> Result { + let public_key = nssa::PublicKey::try_new(value.public_key.data) + .map_err(|_| WalletFfiError::InvalidTypeConversion)?; + Ok(public_key) + } +} diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 74866b28..4d282221 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -95,6 +95,14 @@ typedef enum WalletFfiError { * Serialization/deserialization error */ SERIALIZATION_ERROR = 14, + /** + * Invalid conversion from FFI types to NSSA types + */ + INVALID_TYPE_CONVERSION = 15, + /** + * Invalid Key value + */ + INVALID_KEY_VALUE = 16, /** * Internal error (catch-all) */ @@ -141,6 +149,13 @@ typedef struct FfiProgramId { uint32_t data[8]; } FfiProgramId; +/** + * U128 - 16 bytes little endian + */ +typedef struct FfiU128 { + uint8_t data[16]; +} FfiU128; + /** * Account data structure - C-compatible version of nssa Account. * @@ -152,7 +167,7 @@ typedef struct FfiAccount { /** * Balance as little-endian [u8; 16] */ - uint8_t balance[16]; + struct FfiU128 balance; /** * Pointer to account data bytes */ @@ -164,7 +179,7 @@ typedef struct FfiAccount { /** * Nonce as little-endian [u8; 16] */ - uint8_t nonce[16]; + struct FfiU128 nonce; } FfiAccount; /** diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 0f39c581..9f07cb34 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -175,7 +175,7 @@ pub struct GasConfig { pub gas_limit_runtime: u64, } -#[optfield::optfield(pub WalletConfigOverrides, rewrap, attrs = (derive(Debug, Default)))] +#[optfield::optfield(pub WalletConfigOverrides, rewrap, attrs = (derive(Debug, Default, Clone)))] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WalletConfig { /// Override rust log (env var logging level) diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 58ef47ba..23344107 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -124,6 +124,7 @@ impl TokenHolding { pub struct WalletCore { config_path: PathBuf, + config_overrides: Option, storage: WalletChainStore, storage_path: PathBuf, poller: TxPoller, @@ -186,7 +187,7 @@ impl WalletCore { ) -> Result { let mut config = WalletConfig::from_path_or_initialize_default(&config_path) .with_context(|| format!("Failed to deserialize wallet config at {config_path:#?}"))?; - if let Some(config_overrides) = config_overrides { + if let Some(config_overrides) = config_overrides.clone() { config.apply_overrides(config_overrides); } @@ -205,6 +206,7 @@ impl WalletCore { poller: tx_poller, sequencer_client, last_synced_block, + config_overrides, }) } @@ -543,4 +545,16 @@ impl WalletCore { .insert_private_account_data(affected_account_id, new_acc); } } + + pub fn config_path(&self) -> &PathBuf { + &self.config_path + } + + pub fn storage_path(&self) -> &PathBuf { + &self.storage_path + } + + pub fn config_overrides(&self) -> &Option { + &self.config_overrides + } }