Merge pull request #318 from logos-blockchain/schouhy/add-wallet-ffi-tests

Wallet FFI Tests
This commit is contained in:
Sergio Chouhy 2026-02-05 13:58:38 -03:00 committed by GitHub
commit ce29ca2fd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 813 additions and 38 deletions

4
Cargo.lock generated
View File

@ -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",
]

View File

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

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,
}
@ -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<Self> {
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}")
}

View File

@ -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::<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);
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::<AccountId>().unwrap()).into();
let to: FfiBytes32 = (&ACC_RECEIVER.parse::<AccountId>().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(())
}

View File

@ -14,7 +14,7 @@ mod state;
pub use nssa_core::{
SharedSecretKey,
account::{Account, AccountId},
account::{Account, AccountId, Data},
encryption::EphemeralPublicKey,
program::ProgramId,
};

View File

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

View File

@ -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<u8> = 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

View File

@ -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,
}

View File

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

View File

@ -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<nssa_core::encryption::IncomingViewingPublicKey, WalletFfiError> {
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<u128> for FfiU128 {
fn from(value: u128) -> Self {
Self {
data: value.to_le_bytes(),
}
}
}
impl From<FfiU128> 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<FfiBytes32> for nssa::AccountId {
nssa::AccountId::new(bytes.data)
}
}
impl From<nssa::Account> for FfiAccount {
fn from(value: nssa::Account) -> Self {
// Convert account data to FFI type
let data_vec: Vec<u8> = 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<Self, Self::Error> {
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<nssa::PublicKey> 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<Self, Self::Error> {
let public_key = nssa::PublicKey::try_new(value.public_key.data)
.map_err(|_| WalletFfiError::InvalidTypeConversion)?;
Ok(public_key)
}
}

View File

@ -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;
/**

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,7 +187,7 @@ 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);
}
@ -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<WalletConfigOverrides> {
&self.config_overrides
}
}