diff --git a/Cargo.lock b/Cargo.lock index b637399e..8cd4da9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -821,6 +821,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -1170,6 +1181,25 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "cbindgen" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" +dependencies = [ + "clap 3.2.25", + "heck 0.4.1", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml 0.5.11", +] + [[package]] name = "cc" version = "1.2.49" @@ -1266,6 +1296,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + [[package]] name = "clap" version = "4.5.53" @@ -1284,8 +1329,8 @@ checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", - "clap_lex", - "strsim", + "clap_lex 0.7.6", + "strsim 0.11.1", ] [[package]] @@ -1294,12 +1339,21 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.111", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.7.6" @@ -1578,7 +1632,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.111", ] @@ -1592,7 +1646,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.111", ] @@ -1818,7 +1872,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e92f10a49176cbffacaedabfaa11d51db1ea0f80a83c26e1873b43cd1742c24" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "proc-macro2-diagnostics", ] @@ -2433,12 +2487,27 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -2802,7 +2871,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clap", + "clap 4.5.53", "env_logger", "indexer_service_protocol", "indexer_service_rpc", @@ -2928,7 +2997,7 @@ version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", "windows-sys 0.61.2", ] @@ -3139,7 +3208,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro-crate", "proc-macro2", "quote", @@ -4338,6 +4407,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "overwatch" version = "0.1.0" @@ -4578,7 +4653,7 @@ dependencies = [ name = "program_deployment" version = "0.1.0" dependencies = [ - "clap", + "clap 4.5.53", "nssa", "nssa_core", "tokio", @@ -5388,7 +5463,7 @@ dependencies = [ "strum", "tempfile", "thiserror 2.0.17", - "toml", + "toml 0.8.23", "yaml-rust2", ] @@ -5597,7 +5672,7 @@ dependencies = [ "actix", "actix-web", "anyhow", - "clap", + "clap 4.5.53", "common", "env_logger", "log", @@ -5915,6 +5990,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -5936,7 +6017,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.111", @@ -6081,6 +6162,12 @@ dependencies = [ "risc0-zkvm", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + [[package]] name = "thiserror" version = "1.0.69" @@ -6271,6 +6358,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -6614,7 +6710,7 @@ dependencies = [ "base64", "borsh", "bytemuck", - "clap", + "clap 4.5.53", "common", "env_logger", "futures", @@ -6635,6 +6731,17 @@ dependencies = [ "url", ] +[[package]] +name = "wallet-ffi" +version = "0.1.0" +dependencies = [ + "cbindgen", + "common", + "nssa", + "tokio", + "wallet", +] + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 6d40c7fc..328d23d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "key_protocol", "mempool", "wallet", + "wallet-ffi", "common", "nssa", "nssa/core", @@ -40,6 +41,7 @@ indexer_service = { path = "indexer_service" } indexer_service_protocol = { path = "indexer_service/protocol" } indexer_service_rpc = { path = "indexer_service/rpc" } wallet = { path = "wallet" } +wallet-ffi = { path = "wallet-ffi" } test_program_methods = { path = "test_program_methods" } bedrock_client = { path = "bedrock_client" } indexer_core = { path = "indexer_core" } diff --git a/wallet-ffi/Cargo.toml b/wallet-ffi/Cargo.toml new file mode 100644 index 00000000..bc989fea --- /dev/null +++ b/wallet-ffi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wallet-ffi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +wallet.workspace = true +nssa.workspace = true +common.workspace = true +tokio.workspace = true + +[build-dependencies] +cbindgen = "0.26" diff --git a/wallet-ffi/build.rs b/wallet-ffi/build.rs new file mode 100644 index 00000000..63ee0d9e --- /dev/null +++ b/wallet-ffi/build.rs @@ -0,0 +1,13 @@ +fn main() { + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + + let config = + cbindgen::Config::from_file("cbindgen.toml").expect("Unable to read cbindgen.toml"); + + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_config(config) + .generate() + .expect("Unable to generate bindings") + .write_to_file("wallet_ffi.h"); +} diff --git a/wallet-ffi/cbindgen.toml b/wallet-ffi/cbindgen.toml new file mode 100644 index 00000000..42c46543 --- /dev/null +++ b/wallet-ffi/cbindgen.toml @@ -0,0 +1,40 @@ +language = "C" +header = """ +/** + * NSSA Wallet FFI Bindings + * + * Thread Safety: All functions are thread-safe. The wallet handle can be + * shared across threads, but operations are serialized internally. + * + * Memory Management: + * - Functions returning pointers allocate memory that must be freed + * - Use the corresponding wallet_ffi_free_* function to free memory + * - Never free memory returned by FFI using standard C free() + * + * Error Handling: + * - Functions return WalletFfiError codes + * - On error, call wallet_ffi_get_last_error() for detailed message + * - The error string must be freed with wallet_ffi_free_error_string() + * + * Initialization: + * 1. Call wallet_ffi_init_runtime() before any other function + * 2. Create wallet with wallet_ffi_create_new() or wallet_ffi_open() + * 3. Destroy wallet with wallet_ffi_destroy() when done + */ +""" + +include_guard = "WALLET_FFI_H" +include_version = true +no_includes = false + +[export] +include = ["Ffi.*", "WalletFfiError", "WalletHandle"] + +[enum] +rename_variants = "ScreamingSnakeCase" + +[fn] +rename_args = "None" + +[struct] +rename_fields = "None" diff --git a/wallet-ffi/src/account.rs b/wallet-ffi/src/account.rs new file mode 100644 index 00000000..b99d10cf --- /dev/null +++ b/wallet-ffi/src/account.rs @@ -0,0 +1,395 @@ +//! Account management functions. + +use std::ptr; + +use nssa::AccountId; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + types::{ + FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, FfiProgramId, WalletHandle, + }, + wallet::get_wallet, +}; + +/// Create a new public account. +/// +/// Public accounts use standard transaction signing and are suitable for +/// non-private operations. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `out_account_id`: Output pointer for the new account ID (32 bytes) +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `out_account_id` must be a valid pointer to a `FfiBytes32` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_create_account_public( + handle: *mut WalletHandle, + out_account_id: *mut FfiBytes32, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if out_account_id.is_null() { + print_error("Null output pointer for account_id"); + return WalletFfiError::NullPointer; + } + + let mut wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let (account_id, _chain_index) = wallet.create_new_account_public(None); + + unsafe { + (*out_account_id).data = *account_id.value(); + } + + WalletFfiError::Success +} + +/// Create a new private account. +/// +/// Private accounts use privacy-preserving transactions with nullifiers +/// and commitments. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `out_account_id`: Output pointer for the new account ID (32 bytes) +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `out_account_id` must be a valid pointer to a `FfiBytes32` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_create_account_private( + handle: *mut WalletHandle, + out_account_id: *mut FfiBytes32, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if out_account_id.is_null() { + print_error("Null output pointer for account_id"); + return WalletFfiError::NullPointer; + } + + let mut wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let (account_id, _chain_index) = wallet.create_new_account_private(None); + + unsafe { + (*out_account_id).data = *account_id.value(); + } + + WalletFfiError::Success +} + +/// List all accounts in the wallet. +/// +/// Returns both public and private accounts managed by this wallet. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `out_list`: Output pointer for the account list +/// +/// # Returns +/// - `Success` on successful listing +/// - Error code on failure +/// +/// # Memory +/// The returned list must be freed with `wallet_ffi_free_account_list()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `out_list` must be a valid pointer to a `FfiAccountList` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_list_accounts( + handle: *mut WalletHandle, + out_list: *mut FfiAccountList, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if out_list.is_null() { + print_error("Null output pointer for account list"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let user_data = &wallet.storage().user_data; + let mut entries = Vec::new(); + + // Public accounts from default signing keys (preconfigured) + for account_id in user_data.default_pub_account_signing_keys.keys() { + entries.push(FfiAccountListEntry { + account_id: FfiBytes32::from_account_id(account_id), + is_public: true, + }); + } + + // Public accounts from key tree (generated) + for account_id in user_data.public_key_tree.account_id_map.keys() { + entries.push(FfiAccountListEntry { + account_id: FfiBytes32::from_account_id(account_id), + is_public: true, + }); + } + + // Private accounts from default accounts (preconfigured) + for account_id in user_data.default_user_private_accounts.keys() { + entries.push(FfiAccountListEntry { + account_id: FfiBytes32::from_account_id(account_id), + is_public: false, + }); + } + + // Private accounts from key tree (generated) + for account_id in user_data.private_key_tree.account_id_map.keys() { + entries.push(FfiAccountListEntry { + account_id: FfiBytes32::from_account_id(account_id), + is_public: false, + }); + } + + let count = entries.len(); + + if count == 0 { + unsafe { + (*out_list).entries = ptr::null_mut(); + (*out_list).count = 0; + } + } else { + let entries_boxed = entries.into_boxed_slice(); + let entries_ptr = Box::into_raw(entries_boxed) as *mut FfiAccountListEntry; + + unsafe { + (*out_list).entries = entries_ptr; + (*out_list).count = count; + } + } + + WalletFfiError::Success +} + +/// Free an account list returned by `wallet_ffi_list_accounts`. +/// +/// # Safety +/// The list must be either null or a valid list returned by `wallet_ffi_list_accounts`. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_account_list(list: *mut FfiAccountList) { + if list.is_null() { + return; + } + + unsafe { + let list = &*list; + if !list.entries.is_null() && list.count > 0 { + let slice = std::slice::from_raw_parts_mut(list.entries, list.count); + drop(Box::from_raw(slice as *mut [FfiAccountListEntry])); + } + } +} + +/// Get account balance. +/// +/// For public accounts, this fetches the balance from the network. +/// For private accounts, this returns the locally cached balance. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id`: The account ID (32 bytes) +/// - `is_public`: Whether this is a public account +/// - `out_balance`: Output for balance as little-endian [u8; 16] +/// +/// # Returns +/// - `Success` on successful query +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `out_balance` must be a valid pointer to a `[u8; 16]` array +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_balance( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + is_public: bool, + out_balance: *mut [u8; 16], +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_id.is_null() || out_balance.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let account_id = AccountId::new(unsafe { (*account_id).data }); + + let balance = if is_public { + match block_on(wallet.get_account_balance(account_id)) { + Ok(Ok(b)) => b, + Ok(Err(e)) => { + print_error(format!("Failed to get balance: {}", e)); + return WalletFfiError::NetworkError; + } + Err(e) => return e, + } + } else { + match wallet.get_account_private(&account_id) { + Some(account) => account.balance, + None => { + print_error("Private account not found"); + return WalletFfiError::AccountNotFound; + } + } + }; + + unsafe { + *out_balance = balance.to_le_bytes(); + } + + WalletFfiError::Success +} + +/// Get full public account data from the network. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id`: The account ID (32 bytes) +/// - `out_account`: Output pointer for account data +/// +/// # Returns +/// - `Success` on successful query +/// - Error code on failure +/// +/// # Memory +/// The account data must be freed with `wallet_ffi_free_account_data()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `out_account` must be a valid pointer to a `FfiAccount` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_account_public( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_account: *mut FfiAccount, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_id.is_null() || out_account.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let account_id = AccountId::new(unsafe { (*account_id).data }); + + let account = match block_on(wallet.get_account_public(account_id)) { + Ok(Ok(a)) => a, + Ok(Err(e)) => { + print_error(format!("Failed to get account: {}", e)); + return WalletFfiError::NetworkError; + } + 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; + } + + WalletFfiError::Success +} + +/// Free account data returned by `wallet_ffi_get_account_public`. +/// +/// # Safety +/// The account must be either null or a valid account returned by +/// `wallet_ffi_get_account_public`. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_account_data(account: *mut FfiAccount) { + if account.is_null() { + return; + } + + unsafe { + let account = &*account; + if !account.data.is_null() && account.data_len > 0 { + let slice = std::slice::from_raw_parts_mut(account.data as *mut u8, account.data_len); + drop(Box::from_raw(slice as *mut [u8])); + } + } +} diff --git a/wallet-ffi/src/error.rs b/wallet-ffi/src/error.rs new file mode 100644 index 00000000..ea366475 --- /dev/null +++ b/wallet-ffi/src/error.rs @@ -0,0 +1,46 @@ +//! Error handling for the FFI layer. +//! +//! Uses numeric error codes with error messages printed to stderr. + +/// Error codes returned by FFI functions. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WalletFfiError { + /// Operation completed successfully + Success = 0, + /// A null pointer was passed where a valid pointer was expected + NullPointer = 1, + /// Invalid UTF-8 string + InvalidUtf8 = 2, + /// Wallet handle is not initialized + WalletNotInitialized = 3, + /// Configuration error + ConfigError = 4, + /// Storage/persistence error + StorageError = 5, + /// Network/RPC error + NetworkError = 6, + /// Account not found + AccountNotFound = 7, + /// Key not found for account + KeyNotFound = 8, + /// Insufficient funds for operation + InsufficientFunds = 9, + /// Invalid account ID format + InvalidAccountId = 10, + /// Tokio runtime error + RuntimeError = 11, + /// Password required but not provided + PasswordRequired = 12, + /// Block synchronization error + SyncError = 13, + /// Serialization/deserialization error + SerializationError = 14, + /// Internal error (catch-all) + InternalError = 99, +} + +/// Log an error message to stderr. +pub fn print_error(msg: impl Into) { + eprintln!("[wallet-ffi] {}", msg.into()); +} diff --git a/wallet-ffi/src/keys.rs b/wallet-ffi/src/keys.rs new file mode 100644 index 00000000..e8309a81 --- /dev/null +++ b/wallet-ffi/src/keys.rs @@ -0,0 +1,253 @@ +//! Key retrieval functions. + +use std::ptr; + +use nssa::{AccountId, PublicKey}; + +use crate::{ + error::{print_error, WalletFfiError}, + types::{FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, WalletHandle}, + wallet::get_wallet, +}; + +/// Get the public key for a public account. +/// +/// This returns the public key derived from the account's signing key. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id`: The account ID (32 bytes) +/// - `out_public_key`: Output pointer for the public key +/// +/// # Returns +/// - `Success` on successful retrieval +/// - `KeyNotFound` if the account's key is not in this wallet +/// - Error code on other failures +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `out_public_key` must be a valid pointer to a `FfiPublicAccountKey` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_public_account_key( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_public_key: *mut FfiPublicAccountKey, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_id.is_null() || out_public_key.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let account_id = AccountId::new(unsafe { (*account_id).data }); + + let private_key = match wallet.get_account_public_signing_key(&account_id) { + Some(k) => k, + None => { + print_error("Public account key not found in wallet"); + return WalletFfiError::KeyNotFound; + } + }; + + let public_key = PublicKey::new_from_private_key(private_key); + + unsafe { + (*out_public_key).public_key.data = *public_key.value(); + } + + WalletFfiError::Success +} + +/// Get keys for a private account. +/// +/// Returns the nullifier public key (NPK) and incoming viewing public key (IPK) +/// for the specified private account. These keys are safe to share publicly. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id`: The account ID (32 bytes) +/// - `out_keys`: Output pointer for the key data +/// +/// # Returns +/// - `Success` on successful retrieval +/// - `AccountNotFound` if the private account is not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The keys structure must be freed with `wallet_ffi_free_private_account_keys()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_private_account_keys( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_keys: *mut FfiPrivateAccountKeys, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_id.is_null() || out_keys.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let account_id = AccountId::new(unsafe { (*account_id).data }); + + let (key_chain, _account) = match wallet.storage().user_data.get_private_account(&account_id) { + Some(k) => k, + None => { + print_error("Private account not found in wallet"); + return WalletFfiError::AccountNotFound; + } + }; + + // NPK is a 32-byte array + let npk_bytes = key_chain.nullifer_public_key.0; + + // IPK is a compressed secp256k1 point (33 bytes) + let ipk_bytes = key_chain.incoming_viewing_public_key.to_bytes(); + let ipk_len = ipk_bytes.len(); + let ipk_vec = ipk_bytes.to_vec(); + let ipk_boxed = ipk_vec.into_boxed_slice(); + let ipk_ptr = Box::into_raw(ipk_boxed) as *const u8; + + unsafe { + (*out_keys).nullifier_public_key.data = npk_bytes; + (*out_keys).incoming_viewing_public_key = ipk_ptr; + (*out_keys).incoming_viewing_public_key_len = ipk_len; + } + + WalletFfiError::Success +} + +/// Free private account keys returned by `wallet_ffi_get_private_account_keys`. +/// +/// # Safety +/// The keys must be either null or valid keys returned by +/// `wallet_ffi_get_private_account_keys`. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_private_account_keys(keys: *mut FfiPrivateAccountKeys) { + if keys.is_null() { + return; + } + + unsafe { + let keys = &*keys; + if !keys.incoming_viewing_public_key.is_null() && keys.incoming_viewing_public_key_len > 0 { + let slice = std::slice::from_raw_parts_mut( + keys.incoming_viewing_public_key as *mut u8, + keys.incoming_viewing_public_key_len, + ); + drop(Box::from_raw(slice as *mut [u8])); + } + } +} + +/// Convert an account ID to a Base58 string. +/// +/// # Parameters +/// - `account_id`: The account ID (32 bytes) +/// +/// # Returns +/// - Pointer to null-terminated Base58 string on success +/// - Null pointer on error +/// +/// # Memory +/// The returned string must be freed with `wallet_ffi_free_string()`. +/// +/// # Safety +/// - `account_id` must be a valid pointer to a `FfiBytes32` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_account_id_to_base58( + account_id: *const FfiBytes32, +) -> *mut std::ffi::c_char { + if account_id.is_null() { + print_error("Null account_id pointer"); + return ptr::null_mut(); + } + + let account_id = AccountId::new(unsafe { (*account_id).data }); + let base58_str = account_id.to_string(); + + match std::ffi::CString::new(base58_str) { + Ok(s) => s.into_raw(), + Err(e) => { + print_error(format!("Failed to create C string: {}", e)); + ptr::null_mut() + } + } +} + +/// Parse a Base58 string into an account ID. +/// +/// # Parameters +/// - `base58_str`: Null-terminated Base58 string +/// - `out_account_id`: Output pointer for the account ID (32 bytes) +/// +/// # Returns +/// - `Success` on successful parsing +/// - `InvalidAccountId` if the string is not valid Base58 +/// - Error code on other failures +/// +/// # Safety +/// - `base58_str` must be a valid pointer to a null-terminated C string +/// - `out_account_id` must be a valid pointer to a `FfiBytes32` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_account_id_from_base58( + base58_str: *const std::ffi::c_char, + out_account_id: *mut FfiBytes32, +) -> WalletFfiError { + if base58_str.is_null() || out_account_id.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let c_str = unsafe { std::ffi::CStr::from_ptr(base58_str) }; + let str_slice = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + print_error(format!("Invalid UTF-8: {}", e)); + return WalletFfiError::InvalidUtf8; + } + }; + + let account_id: AccountId = match str_slice.parse() { + Ok(id) => id, + Err(e) => { + print_error(format!("Invalid Base58 account ID: {}", e)); + return WalletFfiError::InvalidAccountId; + } + }; + + unsafe { + (*out_account_id).data = *account_id.value(); + } + + WalletFfiError::Success +} diff --git a/wallet-ffi/src/lib.rs b/wallet-ffi/src/lib.rs new file mode 100644 index 00000000..75032300 --- /dev/null +++ b/wallet-ffi/src/lib.rs @@ -0,0 +1,70 @@ +//! NSSA Wallet FFI Library +//! +//! This crate provides C-compatible bindings for the NSSA wallet functionality. +//! +//! # Usage +//! +//! 1. Initialize the runtime with `wallet_ffi_init_runtime()` +//! 2. Create or open a wallet with `wallet_ffi_create_new()` or `wallet_ffi_open()` +//! 3. Use the wallet functions to manage accounts and transfers +//! 4. Destroy the wallet with `wallet_ffi_destroy()` when done +//! +//! # Thread Safety +//! +//! All functions are thread-safe. The wallet handle uses internal locking +//! to ensure safe concurrent access. +//! +//! # Memory Management +//! +//! - Functions returning pointers allocate memory that must be freed +//! - Use the corresponding `wallet_ffi_free_*` function to free memory +//! - Never free memory returned by FFI using standard C `free()` + +pub mod account; +pub mod error; +pub mod keys; +pub mod sync; +pub mod transfer; +pub mod types; +pub mod wallet; + +// Re-export public types for cbindgen +pub use error::WalletFfiError as FfiError; +use tokio::runtime::Handle; +pub use types::*; + +use crate::error::{print_error, WalletFfiError}; + +/// Get a reference to the global runtime. +pub(crate) fn get_runtime() -> Result { + Handle::try_current().map_err(|_| WalletFfiError::RuntimeError) +} + +/// Run an async future on the global runtime, blocking until completion. +pub(crate) fn block_on(future: F) -> Result { + let runtime = get_runtime()?; + Ok(runtime.block_on(future)) +} + +/// Initialize the global Tokio runtime. +/// +/// This must be called before any async operations (like network calls). +/// Safe to call multiple times - subsequent calls are no-ops. +/// +/// # Returns +/// - `Success` if the runtime was initialized or already exists +/// - `RuntimeError` if runtime creation failed +#[no_mangle] +pub extern "C" fn wallet_ffi_init_runtime() -> WalletFfiError { + let result = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build(); + + match result { + Ok(_) => WalletFfiError::Success, + Err(e) => { + print_error(format!("Failed to initialize runtime: {}", e)); + WalletFfiError::RuntimeError + } + } +} diff --git a/wallet-ffi/src/sync.rs b/wallet-ffi/src/sync.rs new file mode 100644 index 00000000..3979f935 --- /dev/null +++ b/wallet-ffi/src/sync.rs @@ -0,0 +1,151 @@ +//! Block synchronization functions. + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + types::WalletHandle, + wallet::get_wallet, +}; + +/// Synchronize private accounts to a specific block. +/// +/// This scans the blockchain from the last synced block to the specified block, +/// updating private account balances based on any relevant transactions. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `block_id`: Target block number to sync to +/// +/// # Returns +/// - `Success` if synchronization completed +/// - `SyncError` if synchronization failed +/// - Error code on other failures +/// +/// # Note +/// This operation can take a while for large block ranges. The wallet +/// internally uses a progress bar which may output to stdout. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_sync_to_block( + handle: *mut WalletHandle, + block_id: u64, +) -> 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; + } + }; + + match block_on(wallet.sync_to_block(block_id)) { + Ok(Ok(())) => WalletFfiError::Success, + Ok(Err(e)) => { + print_error(format!("Sync failed: {}", e)); + WalletFfiError::SyncError + } + Err(e) => e, + } +} + +/// Get the last synced block number. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `out_block_id`: Output pointer for the block number +/// +/// # Returns +/// - `Success` on success +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `out_block_id` must be a valid pointer to a `u64` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_last_synced_block( + handle: *mut WalletHandle, + out_block_id: *mut u64, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if out_block_id.is_null() { + print_error("Null output pointer"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + unsafe { + *out_block_id = wallet.last_synced_block; + } + + WalletFfiError::Success +} + +/// Get the current block height from the sequencer. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `out_block_height`: Output pointer for the current block height +/// +/// # Returns +/// - `Success` on success +/// - `NetworkError` if the sequencer is unreachable +/// - Error code on other failures +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `out_block_height` must be a valid pointer to a `u64` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_current_block_height( + handle: *mut WalletHandle, + out_block_height: *mut u64, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if out_block_height.is_null() { + print_error("Null output pointer"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + match block_on(wallet.sequencer_client.get_last_block()) { + Ok(Ok(response)) => { + unsafe { + *out_block_height = response.last_block; + } + WalletFfiError::Success + } + Ok(Err(e)) => { + print_error(format!("Failed to get block height: {:?}", e)); + WalletFfiError::NetworkError + } + Err(e) => e, + } +} diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs new file mode 100644 index 00000000..055f0c32 --- /dev/null +++ b/wallet-ffi/src/transfer.rs @@ -0,0 +1,199 @@ +//! Token transfer functions. + +use std::{ffi::CString, ptr}; + +use common::error::ExecutionFailureKind; +use nssa::AccountId; +use wallet::program_facades::native_token_transfer::NativeTokenTransfer; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + types::{FfiBytes32, FfiTransferResult, WalletHandle}, + wallet::get_wallet, +}; + +/// Send a public token transfer. +/// +/// Transfers tokens from one public account to another on the network. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `from`: Source account ID (must be owned by this wallet) +/// - `to`: Destination account ID +/// - `amount`: Amount to transfer as little-endian [u8; 16] +/// - `out_result`: Output pointer for transfer result +/// +/// # Returns +/// - `Success` if the transfer was submitted successfully +/// - `InsufficientFunds` if the source account doesn't have enough balance +/// - `KeyNotFound` if the source account's signing key is not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `from` must be a valid pointer to a `FfiBytes32` struct +/// - `to` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` must be a valid pointer to a `[u8; 16]` array +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_transfer_public( + handle: *mut WalletHandle, + from: *const FfiBytes32, + to: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if from.is_null() || to.is_null() || amount.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let from_id = AccountId::new(unsafe { (*from).data }); + let to_id = AccountId::new(unsafe { (*to).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + let transfer = NativeTokenTransfer(&wallet); + + match block_on(transfer.send_public_transfer(from_id, to_id, amount)) { + Ok(Ok(response)) => { + let tx_hash = CString::new(response.tx_hash) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Ok(Err(e)) => { + print_error(format!("Transfer failed: {:?}", e)); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + match e { + ExecutionFailureKind::InsufficientFundsError => WalletFfiError::InsufficientFunds, + ExecutionFailureKind::KeyNotFoundError => WalletFfiError::KeyNotFound, + ExecutionFailureKind::SequencerError => WalletFfiError::NetworkError, + ExecutionFailureKind::SequencerClientError(_) => WalletFfiError::NetworkError, + _ => WalletFfiError::InternalError, + } + } + Err(e) => e, + } +} + +/// Register a public account on the network. +/// +/// This initializes a public account on the blockchain. The account must be +/// owned by this wallet. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id`: Account ID to register +/// - `out_result`: Output pointer for registration result +/// +/// # Returns +/// - `Success` if the registration was submitted successfully +/// - Error code on failure +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_register_public_account( + handle: *mut WalletHandle, + account_id: *const FfiBytes32, + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_id.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + let account_id = AccountId::new(unsafe { (*account_id).data }); + + let transfer = NativeTokenTransfer(&wallet); + + match block_on(transfer.register_account(account_id)) { + Ok(Ok(response)) => { + let tx_hash = CString::new(response.tx_hash) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Ok(Err(e)) => { + print_error(format!("Registration failed: {:?}", e)); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + match e { + ExecutionFailureKind::KeyNotFoundError => WalletFfiError::KeyNotFound, + ExecutionFailureKind::SequencerError => WalletFfiError::NetworkError, + ExecutionFailureKind::SequencerClientError(_) => WalletFfiError::NetworkError, + _ => WalletFfiError::InternalError, + } + } + Err(e) => e, + } +} + +/// Free a transfer result returned by `wallet_ffi_transfer_public` or +/// `wallet_ffi_register_public_account`. +/// +/// # Safety +/// The result must be either null or a valid result from a transfer function. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult) { + if result.is_null() { + return; + } + + unsafe { + let result = &*result; + if !result.tx_hash.is_null() { + drop(CString::from_raw(result.tx_hash)); + } + } +} diff --git a/wallet-ffi/src/types.rs b/wallet-ffi/src/types.rs new file mode 100644 index 00000000..3bcfd9fd --- /dev/null +++ b/wallet-ffi/src/types.rs @@ -0,0 +1,151 @@ +//! C-compatible type definitions for the FFI layer. + +use std::ffi::c_char; + +/// Opaque pointer to the Wallet instance. +/// +/// This type is never instantiated directly - it's used as an opaque handle +/// to hide the internal wallet structure from C code. +#[repr(C)] +pub struct WalletHandle { + _private: [u8; 0], +} + +/// 32-byte array type for AccountId, keys, hashes, etc. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiBytes32 { + pub data: [u8; 32], +} + +/// Program ID - 8 u32 values (32 bytes total). +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiProgramId { + pub data: [u32; 8], +} + +/// Account data structure - C-compatible version of nssa Account. +/// +/// Note: `balance` and `nonce` are u128 values represented as little-endian +/// byte arrays since C doesn't have native u128 support. +#[repr(C)] +pub struct FfiAccount { + pub program_owner: FfiProgramId, + /// Balance as little-endian [u8; 16] + pub balance: [u8; 16], + /// 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], +} + +impl Default for FfiAccount { + fn default() -> Self { + Self { + program_owner: FfiProgramId::default(), + balance: [0u8; 16], + data: std::ptr::null(), + data_len: 0, + nonce: [0u8; 16], + } + } +} + +/// Public keys for a private account (safe to expose). +#[repr(C)] +pub struct FfiPrivateAccountKeys { + /// Nullifier public key (32 bytes) + pub nullifier_public_key: FfiBytes32, + /// Incoming viewing public key (compressed secp256k1 point) + pub incoming_viewing_public_key: *const u8, + /// Length of incoming viewing public key (typically 33 bytes) + pub incoming_viewing_public_key_len: usize, +} + +impl Default for FfiPrivateAccountKeys { + fn default() -> Self { + Self { + nullifier_public_key: FfiBytes32::default(), + incoming_viewing_public_key: std::ptr::null(), + incoming_viewing_public_key_len: 0, + } + } +} + +/// Public key info for a public account. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiPublicAccountKey { + pub public_key: FfiBytes32, +} + +/// Single entry in the account list. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FfiAccountListEntry { + pub account_id: FfiBytes32, + pub is_public: bool, +} + +/// List of accounts returned by wallet_ffi_list_accounts. +#[repr(C)] +pub struct FfiAccountList { + pub entries: *mut FfiAccountListEntry, + pub count: usize, +} + +impl Default for FfiAccountList { + fn default() -> Self { + Self { + entries: std::ptr::null_mut(), + count: 0, + } + } +} + +/// Result of a transfer operation. +#[repr(C)] +pub struct FfiTransferResult { + /// Transaction hash (null-terminated string, or null on failure) + pub tx_hash: *mut c_char, + /// Whether the transfer succeeded + pub success: bool, +} + +impl Default for FfiTransferResult { + fn default() -> Self { + Self { + tx_hash: std::ptr::null_mut(), + success: false, + } + } +} + +// Helper functions to convert between Rust and FFI types + +impl FfiBytes32 { + /// Create from a 32-byte array. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self { data: bytes } + } + + /// Create from an AccountId. + pub fn from_account_id(id: &nssa::AccountId) -> Self { + Self { data: *id.value() } + } +} + +impl From<&nssa::AccountId> for FfiBytes32 { + fn from(id: &nssa::AccountId) -> Self { + Self::from_account_id(id) + } +} + +impl From for nssa::AccountId { + fn from(bytes: FfiBytes32) -> Self { + nssa::AccountId::new(bytes.data) + } +} diff --git a/wallet-ffi/src/wallet.rs b/wallet-ffi/src/wallet.rs new file mode 100644 index 00000000..6f817f8e --- /dev/null +++ b/wallet-ffi/src/wallet.rs @@ -0,0 +1,279 @@ +//! Wallet lifecycle management functions. + +use std::{ + ffi::{c_char, CStr}, + path::PathBuf, + ptr, + sync::Mutex, +}; + +use wallet::WalletCore; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + types::WalletHandle, +}; + +/// Internal wrapper around WalletCore with mutex for thread safety. +pub(crate) struct WalletWrapper { + pub core: Mutex, +} + +/// Helper to get the wallet wrapper from an opaque handle. +pub(crate) fn get_wallet( + handle: *mut WalletHandle, +) -> Result<&'static WalletWrapper, WalletFfiError> { + if handle.is_null() { + print_error("Null wallet handle"); + return Err(WalletFfiError::NullPointer); + } + Ok(unsafe { &*(handle as *mut WalletWrapper) }) +} + +/// Helper to get a mutable reference to the wallet wrapper. +#[allow(dead_code)] +pub(crate) fn get_wallet_mut( + handle: *mut WalletHandle, +) -> Result<&'static mut WalletWrapper, WalletFfiError> { + if handle.is_null() { + print_error("Null wallet handle"); + return Err(WalletFfiError::NullPointer); + } + Ok(unsafe { &mut *(handle as *mut WalletWrapper) }) +} + +/// Helper to convert a C string to a Rust PathBuf. +fn c_str_to_path(ptr: *const c_char, name: &str) -> Result { + if ptr.is_null() { + print_error(format!("Null pointer for {}", name)); + return Err(WalletFfiError::NullPointer); + } + + let c_str = unsafe { CStr::from_ptr(ptr) }; + match c_str.to_str() { + Ok(s) => Ok(PathBuf::from(s)), + Err(e) => { + print_error(format!("Invalid UTF-8 in {}: {}", name, e)); + Err(WalletFfiError::InvalidUtf8) + } + } +} + +/// Helper to convert a C string to a Rust String. +fn c_str_to_string(ptr: *const c_char, name: &str) -> Result { + if ptr.is_null() { + print_error(format!("Null pointer for {}", name)); + return Err(WalletFfiError::NullPointer); + } + + let c_str = unsafe { CStr::from_ptr(ptr) }; + match c_str.to_str() { + Ok(s) => Ok(s.to_string()), + Err(e) => { + print_error(format!("Invalid UTF-8 in {}: {}", name, e)); + Err(WalletFfiError::InvalidUtf8) + } + } +} + +/// Create a new wallet with fresh storage. +/// +/// This initializes a new wallet with a new seed derived from the password. +/// Use this for first-time wallet creation. +/// +/// # Parameters +/// - `config_path`: Path to the wallet configuration file (JSON) +/// - `storage_path`: Path where wallet data will be stored +/// - `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) +/// +/// # Safety +/// All string parameters must be valid null-terminated UTF-8 strings. +#[no_mangle] +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 { + let config_path = match c_str_to_path(config_path, "config_path") { + Ok(p) => p, + Err(_) => return ptr::null_mut(), + }; + + let storage_path = match c_str_to_path(storage_path, "storage_path") { + Ok(p) => p, + Err(_) => return ptr::null_mut(), + }; + + let password = match c_str_to_string(password, "password") { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + + match WalletCore::new_init_storage(config_path, storage_path, None, password) { + Ok(core) => { + let wrapper = Box::new(WalletWrapper { + core: Mutex::new(core), + }); + Box::into_raw(wrapper) as *mut WalletHandle + } + Err(e) => { + print_error(format!("Failed to create wallet: {}", e)); + ptr::null_mut() + } + } +} + +/// Open an existing wallet from storage. +/// +/// This loads a wallet that was previously created with `wallet_ffi_create_new()`. +/// +/// # Parameters +/// - `config_path`: Path to the wallet configuration file (JSON) +/// - `storage_path`: Path where wallet data is stored +/// +/// # Returns +/// - Opaque wallet handle on success +/// - Null pointer on error (call `wallet_ffi_get_last_error()` for details) +/// +/// # Safety +/// All string parameters must be valid null-terminated UTF-8 strings. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_open( + config_path: *const c_char, + storage_path: *const c_char, +) -> *mut WalletHandle { + let config_path = match c_str_to_path(config_path, "config_path") { + Ok(p) => p, + Err(_) => return ptr::null_mut(), + }; + + let storage_path = match c_str_to_path(storage_path, "storage_path") { + Ok(p) => p, + Err(_) => return ptr::null_mut(), + }; + + match WalletCore::new_update_chain(config_path, storage_path, None) { + Ok(core) => { + let wrapper = Box::new(WalletWrapper { + core: Mutex::new(core), + }); + Box::into_raw(wrapper) as *mut WalletHandle + } + Err(e) => { + print_error(format!("Failed to open wallet: {}", e)); + ptr::null_mut() + } + } +} + +/// Destroy a wallet handle and free its resources. +/// +/// After calling this function, the handle is invalid and must not be used. +/// +/// # Safety +/// - The handle must be either null or a valid handle from `wallet_ffi_create_new()` or +/// `wallet_ffi_open()`. +/// - The handle must not be used after this call. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_destroy(handle: *mut WalletHandle) { + if !handle.is_null() { + unsafe { + drop(Box::from_raw(handle as *mut WalletWrapper)); + } + } +} + +/// Save wallet state to persistent storage. +/// +/// This should be called periodically or after important operations to ensure +/// wallet data is persisted to disk. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// +/// # Returns +/// - `Success` on successful save +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_save(handle: *mut WalletHandle) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return WalletFfiError::InternalError; + } + }; + + match block_on(wallet.store_persistent_data()) { + Ok(Ok(())) => WalletFfiError::Success, + Ok(Err(e)) => { + print_error(format!("Failed to save wallet: {}", e)); + WalletFfiError::StorageError + } + Err(e) => e, + } +} + +/// Get the sequencer address from the wallet configuration. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// +/// # Returns +/// - Pointer to null-terminated string on success (caller must free with +/// `wallet_ffi_free_string()`) +/// - Null pointer on error +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_sequencer_addr(handle: *mut WalletHandle) -> *mut c_char { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(_) => return ptr::null_mut(), + }; + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {}", e)); + return ptr::null_mut(); + } + }; + + let addr = wallet.config().sequencer_addr.clone(); + + match std::ffi::CString::new(addr) { + Ok(s) => s.into_raw(), + Err(e) => { + print_error(format!("Invalid sequencer address: {}", e)); + ptr::null_mut() + } + } +} + +/// Free a string returned by wallet FFI functions. +/// +/// # Safety +/// The pointer must be either null or a valid string returned by an FFI function. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_string(ptr: *mut c_char) { + if !ptr.is_null() { + unsafe { + drop(std::ffi::CString::from_raw(ptr)); + } + } +} diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h new file mode 100644 index 00000000..4786e5cc --- /dev/null +++ b/wallet-ffi/wallet_ffi.h @@ -0,0 +1,676 @@ +/** + * NSSA Wallet FFI Bindings + * + * Thread Safety: All functions are thread-safe. The wallet handle can be + * shared across threads, but operations are serialized internally. + * + * Memory Management: + * - Functions returning pointers allocate memory that must be freed + * - Use the corresponding wallet_ffi_free_* function to free memory + * - Never free memory returned by FFI using standard C free() + * + * Error Handling: + * - Functions return WalletFfiError codes + * - On error, call wallet_ffi_get_last_error() for detailed message + * - The error string must be freed with wallet_ffi_free_error_string() + * + * Initialization: + * 1. Call wallet_ffi_init_runtime() before any other function + * 2. Create wallet with wallet_ffi_create_new() or wallet_ffi_open() + * 3. Destroy wallet with wallet_ffi_destroy() when done + */ + + +#ifndef WALLET_FFI_H +#define WALLET_FFI_H + +/* Generated with cbindgen:0.26.0 */ + +#include +#include +#include +#include + +/** + * Error codes returned by FFI functions. + */ +typedef enum WalletFfiError { + /** + * Operation completed successfully + */ + SUCCESS = 0, + /** + * A null pointer was passed where a valid pointer was expected + */ + NULL_POINTER = 1, + /** + * Invalid UTF-8 string + */ + INVALID_UTF8 = 2, + /** + * Wallet handle is not initialized + */ + WALLET_NOT_INITIALIZED = 3, + /** + * Configuration error + */ + CONFIG_ERROR = 4, + /** + * Storage/persistence error + */ + STORAGE_ERROR = 5, + /** + * Network/RPC error + */ + NETWORK_ERROR = 6, + /** + * Account not found + */ + ACCOUNT_NOT_FOUND = 7, + /** + * Key not found for account + */ + KEY_NOT_FOUND = 8, + /** + * Insufficient funds for operation + */ + INSUFFICIENT_FUNDS = 9, + /** + * Invalid account ID format + */ + INVALID_ACCOUNT_ID = 10, + /** + * Tokio runtime error + */ + RUNTIME_ERROR = 11, + /** + * Password required but not provided + */ + PASSWORD_REQUIRED = 12, + /** + * Block synchronization error + */ + SYNC_ERROR = 13, + /** + * Serialization/deserialization error + */ + SERIALIZATION_ERROR = 14, + /** + * Internal error (catch-all) + */ + INTERNAL_ERROR = 99, +} WalletFfiError; + +/** + * Opaque pointer to the Wallet instance. + * + * This type is never instantiated directly - it's used as an opaque handle + * to hide the internal wallet structure from C code. + */ +typedef struct WalletHandle { + uint8_t _private[0]; +} WalletHandle; + +/** + * 32-byte array type for AccountId, keys, hashes, etc. + */ +typedef struct FfiBytes32 { + uint8_t data[32]; +} FfiBytes32; + +/** + * Single entry in the account list. + */ +typedef struct FfiAccountListEntry { + struct FfiBytes32 account_id; + bool is_public; +} FfiAccountListEntry; + +/** + * List of accounts returned by wallet_ffi_list_accounts. + */ +typedef struct FfiAccountList { + struct FfiAccountListEntry *entries; + uintptr_t count; +} FfiAccountList; + +/** + * Program ID - 8 u32 values (32 bytes total). + */ +typedef struct FfiProgramId { + uint32_t data[8]; +} FfiProgramId; + +/** + * Account data structure - C-compatible version of nssa Account. + * + * Note: `balance` and `nonce` are u128 values represented as little-endian + * byte arrays since C doesn't have native u128 support. + */ +typedef struct FfiAccount { + struct FfiProgramId program_owner; + /** + * Balance as little-endian [u8; 16] + */ + uint8_t balance[16]; + /** + * Pointer to account data bytes + */ + const uint8_t *data; + /** + * Length of account data + */ + uintptr_t data_len; + /** + * Nonce as little-endian [u8; 16] + */ + uint8_t nonce[16]; +} FfiAccount; + +/** + * Public key info for a public account. + */ +typedef struct FfiPublicAccountKey { + struct FfiBytes32 public_key; +} FfiPublicAccountKey; + +/** + * Public keys for a private account (safe to expose). + */ +typedef struct FfiPrivateAccountKeys { + /** + * Nullifier public key (32 bytes) + */ + struct FfiBytes32 nullifier_public_key; + /** + * Incoming viewing public key (compressed secp256k1 point) + */ + const uint8_t *incoming_viewing_public_key; + /** + * Length of incoming viewing public key (typically 33 bytes) + */ + uintptr_t incoming_viewing_public_key_len; +} FfiPrivateAccountKeys; + +/** + * Result of a transfer operation. + */ +typedef struct FfiTransferResult { + /** + * Transaction hash (null-terminated string, or null on failure) + */ + char *tx_hash; + /** + * Whether the transfer succeeded + */ + bool success; +} FfiTransferResult; + +/** + * Initialize the global Tokio runtime. + * + * This must be called before any async operations (like network calls). + * Safe to call multiple times - subsequent calls are no-ops. + * + * # Returns + * - `Success` if the runtime was initialized or already exists + * - `RuntimeError` if runtime creation failed + */ +enum WalletFfiError wallet_ffi_init_runtime(void); + +/** + * Create a new public account. + * + * Public accounts use standard transaction signing and are suitable for + * non-private operations. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `out_account_id`: Output pointer for the new account ID (32 bytes) + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `out_account_id` must be a valid pointer to a `FfiBytes32` struct + */ +enum WalletFfiError wallet_ffi_create_account_public(struct WalletHandle *handle, + struct FfiBytes32 *out_account_id); + +/** + * Create a new private account. + * + * Private accounts use privacy-preserving transactions with nullifiers + * and commitments. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `out_account_id`: Output pointer for the new account ID (32 bytes) + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `out_account_id` must be a valid pointer to a `FfiBytes32` struct + */ +enum WalletFfiError wallet_ffi_create_account_private(struct WalletHandle *handle, + struct FfiBytes32 *out_account_id); + +/** + * List all accounts in the wallet. + * + * Returns both public and private accounts managed by this wallet. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `out_list`: Output pointer for the account list + * + * # Returns + * - `Success` on successful listing + * - Error code on failure + * + * # Memory + * The returned list must be freed with `wallet_ffi_free_account_list()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `out_list` must be a valid pointer to a `FfiAccountList` struct + */ +enum WalletFfiError wallet_ffi_list_accounts(struct WalletHandle *handle, + struct FfiAccountList *out_list); + +/** + * Free an account list returned by `wallet_ffi_list_accounts`. + * + * # Safety + * The list must be either null or a valid list returned by `wallet_ffi_list_accounts`. + */ +void wallet_ffi_free_account_list(struct FfiAccountList *list); + +/** + * Get account balance. + * + * For public accounts, this fetches the balance from the network. + * For private accounts, this returns the locally cached balance. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: The account ID (32 bytes) + * - `is_public`: Whether this is a public account + * - `out_balance`: Output for balance as little-endian [u8; 16] + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_balance` must be a valid pointer to a `[u8; 16]` array + */ +enum WalletFfiError wallet_ffi_get_balance(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + bool is_public, + uint8_t (*out_balance)[16]); + +/** + * Get full public account data from the network. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: The account ID (32 bytes) + * - `out_account`: Output pointer for account data + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Memory + * The account data must be freed with `wallet_ffi_free_account_data()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_account` must be a valid pointer to a `FfiAccount` struct + */ +enum WalletFfiError wallet_ffi_get_account_public(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + struct FfiAccount *out_account); + +/** + * Free account data returned by `wallet_ffi_get_account_public`. + * + * # Safety + * The account must be either null or a valid account returned by + * `wallet_ffi_get_account_public`. + */ +void wallet_ffi_free_account_data(struct FfiAccount *account); + +/** + * Get the public key for a public account. + * + * This returns the public key derived from the account's signing key. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: The account ID (32 bytes) + * - `out_public_key`: Output pointer for the public key + * + * # Returns + * - `Success` on successful retrieval + * - `KeyNotFound` if the account's key is not in this wallet + * - Error code on other failures + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_public_key` must be a valid pointer to a `FfiPublicAccountKey` struct + */ +enum WalletFfiError wallet_ffi_get_public_account_key(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + struct FfiPublicAccountKey *out_public_key); + +/** + * Get keys for a private account. + * + * Returns the nullifier public key (NPK) and incoming viewing public key (IPK) + * for the specified private account. These keys are safe to share publicly. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: The account ID (32 bytes) + * - `out_keys`: Output pointer for the key data + * + * # Returns + * - `Success` on successful retrieval + * - `AccountNotFound` if the private account is not in this wallet + * - Error code on other failures + * + * # Memory + * The keys structure must be freed with `wallet_ffi_free_private_account_keys()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_keys` must be a valid pointer to a `FfiPrivateAccountKeys` struct + */ +enum WalletFfiError wallet_ffi_get_private_account_keys(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + struct FfiPrivateAccountKeys *out_keys); + +/** + * Free private account keys returned by `wallet_ffi_get_private_account_keys`. + * + * # Safety + * The keys must be either null or valid keys returned by + * `wallet_ffi_get_private_account_keys`. + */ +void wallet_ffi_free_private_account_keys(struct FfiPrivateAccountKeys *keys); + +/** + * Convert an account ID to a Base58 string. + * + * # Parameters + * - `account_id`: The account ID (32 bytes) + * + * # Returns + * - Pointer to null-terminated Base58 string on success + * - Null pointer on error + * + * # Memory + * The returned string must be freed with `wallet_ffi_free_string()`. + * + * # Safety + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + */ +char *wallet_ffi_account_id_to_base58(const struct FfiBytes32 *account_id); + +/** + * Parse a Base58 string into an account ID. + * + * # Parameters + * - `base58_str`: Null-terminated Base58 string + * - `out_account_id`: Output pointer for the account ID (32 bytes) + * + * # Returns + * - `Success` on successful parsing + * - `InvalidAccountId` if the string is not valid Base58 + * - Error code on other failures + * + * # Safety + * - `base58_str` must be a valid pointer to a null-terminated C string + * - `out_account_id` must be a valid pointer to a `FfiBytes32` struct + */ +enum WalletFfiError wallet_ffi_account_id_from_base58(const char *base58_str, + struct FfiBytes32 *out_account_id); + +/** + * Synchronize private accounts to a specific block. + * + * This scans the blockchain from the last synced block to the specified block, + * updating private account balances based on any relevant transactions. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `block_id`: Target block number to sync to + * + * # Returns + * - `Success` if synchronization completed + * - `SyncError` if synchronization failed + * - Error code on other failures + * + * # Note + * This operation can take a while for large block ranges. The wallet + * internally uses a progress bar which may output to stdout. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + */ +enum WalletFfiError wallet_ffi_sync_to_block(struct WalletHandle *handle, uint64_t block_id); + +/** + * Get the last synced block number. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `out_block_id`: Output pointer for the block number + * + * # Returns + * - `Success` on success + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `out_block_id` must be a valid pointer to a `u64` + */ +enum WalletFfiError wallet_ffi_get_last_synced_block(struct WalletHandle *handle, + uint64_t *out_block_id); + +/** + * Get the current block height from the sequencer. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `out_block_height`: Output pointer for the current block height + * + * # Returns + * - `Success` on success + * - `NetworkError` if the sequencer is unreachable + * - Error code on other failures + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `out_block_height` must be a valid pointer to a `u64` + */ +enum WalletFfiError wallet_ffi_get_current_block_height(struct WalletHandle *handle, + uint64_t *out_block_height); + +/** + * Send a public token transfer. + * + * Transfers tokens from one public account to another on the network. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source account ID (must be owned by this wallet) + * - `to`: Destination account ID + * - `amount`: Amount to transfer as little-endian [u8; 16] + * - `out_result`: Output pointer for transfer result + * + * # Returns + * - `Success` if the transfer was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if the source account's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `to` must be a valid pointer to a `FfiBytes32` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle, + const struct FfiBytes32 *from, + const struct FfiBytes32 *to, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + +/** + * Register a public account on the network. + * + * This initializes a public account on the blockchain. The account must be + * owned by this wallet. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: Account ID to register + * - `out_result`: Output pointer for registration result + * + * # Returns + * - `Success` if the registration was submitted successfully + * - Error code on failure + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_id` must be a valid pointer to a `FfiBytes32` struct + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_register_public_account(struct WalletHandle *handle, + const struct FfiBytes32 *account_id, + struct FfiTransferResult *out_result); + +/** + * Free a transfer result returned by `wallet_ffi_transfer_public` or + * `wallet_ffi_register_public_account`. + * + * # Safety + * The result must be either null or a valid result from a transfer function. + */ +void wallet_ffi_free_transfer_result(struct FfiTransferResult *result); + +/** + * Create a new wallet with fresh storage. + * + * This initializes a new wallet with a new seed derived from the password. + * Use this for first-time wallet creation. + * + * # Parameters + * - `config_path`: Path to the wallet configuration file (JSON) + * - `storage_path`: Path where wallet data will be stored + * - `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) + * + * # 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); + +/** + * Open an existing wallet from storage. + * + * This loads a wallet that was previously created with `wallet_ffi_create_new()`. + * + * # Parameters + * - `config_path`: Path to the wallet configuration file (JSON) + * - `storage_path`: Path where wallet data is stored + * + * # Returns + * - Opaque wallet handle on success + * - Null pointer 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_open(const char *config_path, const char *storage_path); + +/** + * Destroy a wallet handle and free its resources. + * + * After calling this function, the handle is invalid and must not be used. + * + * # Safety + * - The handle must be either null or a valid handle from `wallet_ffi_create_new()` or + * `wallet_ffi_open()`. + * - The handle must not be used after this call. + */ +void wallet_ffi_destroy(struct WalletHandle *handle); + +/** + * Save wallet state to persistent storage. + * + * This should be called periodically or after important operations to ensure + * wallet data is persisted to disk. + * + * # Parameters + * - `handle`: Valid wallet handle + * + * # Returns + * - `Success` on successful save + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + */ +enum WalletFfiError wallet_ffi_save(struct WalletHandle *handle); + +/** + * Get the sequencer address from the wallet configuration. + * + * # Parameters + * - `handle`: Valid wallet handle + * + * # Returns + * - Pointer to null-terminated string on success (caller must free with + * `wallet_ffi_free_string()`) + * - Null pointer on error + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + */ +char *wallet_ffi_get_sequencer_addr(struct WalletHandle *handle); + +/** + * Free a string returned by wallet FFI functions. + * + * # Safety + * The pointer must be either null or a valid string returned by an FFI function. + */ +void wallet_ffi_free_string(char *ptr); + +#endif /* WALLET_FFI_H */