move ffi tests to integration tests

This commit is contained in:
Sergio Chouhy 2026-02-03 19:04:41 -03:00
parent 35b469d738
commit 50b253fa00
7 changed files with 666 additions and 455 deletions

663
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,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

View File

@ -44,6 +44,7 @@ pub struct TestContext {
indexer_loop_handle: Option<JoinHandle<Result<()>>>,
sequencer_client: SequencerClient,
wallet: WalletCore,
wallet_password: String,
_temp_sequencer_dir: TempDir,
_temp_wallet_dir: TempDir,
}
@ -62,6 +63,12 @@ impl TestContext {
Self::new_with_sequencer_and_maybe_indexer_configs(sequencer_config, None).await
}
pub fn new_blocking() -> Result<Self> {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(Self::new())
}
/// Create new test context in local bedrock node attached mode.
pub async fn new_bedrock_local_attached() -> Result<Self> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
@ -114,7 +121,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 +149,7 @@ impl TestContext {
wallet,
_temp_sequencer_dir: temp_sequencer_dir,
_temp_wallet_dir: temp_wallet_dir,
wallet_password,
})
} else {
Ok(Self {
@ -153,6 +161,7 @@ impl TestContext {
wallet,
_temp_sequencer_dir: temp_sequencer_dir,
_temp_wallet_dir: temp_wallet_dir,
wallet_password,
})
}
}
@ -193,7 +202,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 +220,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 +233,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 +241,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 +269,7 @@ impl Drop for TestContext {
wallet: _,
_temp_sequencer_dir,
_temp_wallet_dir,
wallet_password: _,
} = self;
sequencer_loop_handle.abort();

View File

@ -0,0 +1,285 @@
use std::{
collections::HashSet,
ffi::{CString, c_char},
io::Write,
};
use anyhow::Result;
use integration_tests::{ACC_SENDER, TestContext};
use nssa::AccountId;
use tempfile::tempdir;
use wallet::WalletCore;
use wallet_ffi::{FfiAccountList, FfiBytes32, 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_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 new_wallet_from_test_context(ctx: &TestContext) -> *mut WalletHandle {
let tempdir = tempfile::tempdir().unwrap();
let temp_config_path = tempdir.path().join("wallet_config.json");
let mut config = ctx.wallet().config().to_owned();
if let Some(config_overrides) = ctx.wallet().config_overrides().clone() {
config.apply_overrides(config_overrides);
}
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temp_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(temp_config_path.to_str().unwrap()).unwrap();
let storage_path = CString::new(ctx.wallet().storage_path().to_str().unwrap()).unwrap();
let password = CString::new(ctx.wallet_password()).unwrap();
unsafe {
wallet_ffi_create_new(
config_path.as_ptr(),
storage_path.as_ptr(),
password.as_ptr(),
)
}
}
fn new_wallet_ffi_on_tempdir_for_tests(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_for_tests(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_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_for_tests(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_on_tempdir_for_tests(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);
}
account_ids
};
assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust)
}
#[test]
fn test_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_for_tests(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_on_tempdir_for_tests(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);
}
account_ids
};
assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust)
}
#[test]
fn test_list_accounts() {
let password = "password_for_tests";
// Create the wallet FFI
let wallet_ffi_handle = unsafe {
let handle = new_wallet_ffi_on_tempdir_for_tests(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_for_tests(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::<Vec<_>>();
// 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::<HashSet<_>>(),
wallet_ffi_account_list_slice
.iter()
.map(|entry| &entry.account_id.data)
.collect::<HashSet<_>>()
);
// 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) };
}
#[test]
fn test_wallet_ffi_get_balance_public() -> Result<()> {
let ctx = TestContext::new_blocking()?;
let account_id: AccountId = ACC_SENDER.parse().unwrap();
let wallet_ffi_handle = new_wallet_from_test_context(&ctx);
let balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let ffi_account_id = FfiBytes32::from_bytes(*account_id.value());
let result = wallet_ffi_get_balance(
wallet_ffi_handle,
(&ffi_account_id) as *const FfiBytes32,
true,
(&mut out_balance) as *mut [u8; 16],
);
println!("result: {:?}", result);
out_balance
};
println!("balance: {balance:?}");
Ok(())
}

View File

@ -1,129 +0,0 @@
use std::{
ffi::{c_char, CString},
path::Path,
};
use tokio::runtime::Handle;
use wallet::WalletCore;
use wallet_ffi::{error, FfiBytes32, FfiError, WalletHandle};
extern "C" {
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_create_new(
config_path: *const c_char,
storage_path: *const c_char,
password: *const c_char,
) -> *mut WalletHandle;
fn wallet_ffi_save(handle: *mut WalletHandle) -> error::WalletFfiError;
}
use tempfile::tempdir;
unsafe fn new_wallet_ffi_for_tests(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();
wallet_ffi_create_new(
config_path_c.as_ptr(),
storage_path_c.as_ptr(),
password.as_ptr(),
)
}
fn new_wallet_rust_for_tests(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_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_for_tests(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_for_tests(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);
}
account_ids
};
assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust)
}
#[test]
fn test_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_for_tests(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_for_tests(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);
}
account_ids
};
assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust)
}

View File

@ -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)

View File

@ -124,6 +124,7 @@ impl TokenHolding {
pub struct WalletCore {
config_path: PathBuf,
config_overrides: Option<WalletConfigOverrides>,
storage: WalletChainStore,
storage_path: PathBuf,
poller: TxPoller,
@ -186,10 +187,11 @@ impl WalletCore {
) -> Result<Self> {
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);
}
println!("sequencer url: {}", config.sequencer_addr);
let sequencer_client = Arc::new(SequencerClient::new_with_auth(
Url::parse(&config.sequencer_addr)?,
config.basic_auth.clone(),
@ -205,6 +207,7 @@ impl WalletCore {
poller: tx_poller,
sequencer_client,
last_synced_block,
config_overrides,
})
}
@ -543,4 +546,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<WalletConfigOverrides> {
&self.config_overrides
}
}