diff --git a/Cargo.lock b/Cargo.lock index 47f19d1c..2178d13a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10915,6 +10915,7 @@ dependencies = [ "key_protocol", "lee", "lee_core", + "risc0-zkvm", "sequencer_service_rpc", "serde_json", "tempfile", diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 70e6d44d..24f2a9c8 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -7,6 +7,7 @@ clippy::undocumented_unsafe_blocks, clippy::multiple_unsafe_ops_per_block, clippy::shadow_unrelated, + clippy::as_conversions, reason = "We don't care about these in tests" )] @@ -20,14 +21,18 @@ use std::{ use anyhow::Result; use integration_tests::{BlockingTestContext, TIME_TO_WAIT_FOR_BLOCK_SECONDS}; -use lee::{Account, AccountId, PrivateKey, PublicKey, program::Program}; +use lee::{ + Account, AccountId, PrivateKey, PublicKey, + privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, +}; use lee_core::program::DEFAULT_PROGRAM_ID; use log::info; use tempfile::tempdir; use wallet::account::HumanReadableAccount; use wallet_ffi::{ - FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, - FfiTransferResult, FfiU128, WalletHandle, error, + FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, + FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error, + generic_transaction::{FfiProgramWithDependencies, FfiTransactionResult}, }; unsafe extern "C" { @@ -180,6 +185,42 @@ unsafe extern "C" { handle: *mut WalletHandle, out_block_height: *mut u64, ) -> error::WalletFfiError; + + fn wallet_ffi_resolve_public_account( + account_id: FfiBytes32, + needs_sign: bool, + out_account_identity: *mut FfiAccountIdentity, + ) -> error::WalletFfiError; + + fn wallet_ffi_send_generic_public_transaction( + handle: *mut WalletHandle, + account_identities: *const FfiAccountIdentity, + account_identities_size: usize, + instruction_words: *const u32, + instruction_words_size: usize, + program_with_dependencies: *const FfiProgramWithDependencies, + out_result: *mut FfiTransactionResult, + ) -> error::WalletFfiError; + + fn wallet_ffi_resolve_private_account( + handle: *mut WalletHandle, + account_id: FfiBytes32, + out_account_identity: *mut FfiAccountIdentity, + ) -> error::WalletFfiError; + + fn wallet_ffi_send_generic_private_transaction( + handle: *mut WalletHandle, + account_identities: *const FfiAccountIdentity, + account_identities_size: usize, + instruction_words: *const u32, + instruction_words_size: usize, + program_with_dependencies: *const FfiProgramWithDependencies, + out_result: *mut FfiTransactionResult, + ) -> error::WalletFfiError; + + fn wallet_ffi_free_transaction_result(result: *mut FfiTransactionResult); + + fn wallet_ffi_free_account_identity(account_identity: *mut FfiAccountIdentity); } fn new_wallet_ffi_with_test_context_config( @@ -1068,3 +1109,201 @@ fn test_wallet_ffi_transfer_private() -> Result<()> { Ok(()) } + +#[test] +fn test_wallet_ffi_transfer_generic_public() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); + let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into(); + let amount = 100_u128; + + let mut transaction_result = FfiTransactionResult::default(); + + let mut from_account_identity = FfiAccountIdentity::default(); + let mut to_account_identity = FfiAccountIdentity::default(); + + unsafe { + wallet_ffi_resolve_public_account(from, true, &raw mut from_account_identity).unwrap(); + } + + unsafe { + wallet_ffi_resolve_public_account(to, true, &raw mut to_account_identity).unwrap(); + } + + let ffi_accs = vec![from_account_identity, to_account_identity]; + let account_identities_size = ffi_accs.len(); + let account_identities = + Box::into_raw(ffi_accs.into_boxed_slice()) as *const FfiAccountIdentity; + + let instruction_data = + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount, + }) + .unwrap(); + let instruction_words_size = instruction_data.len(); + let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32; + + let program: ProgramWithDependencies = Program::authenticated_transfer_program().into(); + let program_with_dependencies: FfiProgramWithDependencies = program.into(); + + unsafe { + wallet_ffi_send_generic_public_transaction( + wallet_ffi_handle, + account_identities, + account_identities_size, + instruction_words, + instruction_words_size, + &raw const program_with_dependencies, + &raw mut transaction_result, + ) + .unwrap(); + } + + 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]; + wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const from, + true, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + + let to_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_balance(wallet_ffi_handle, &raw const to, true, &raw mut out_balance) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(from_balance, 9900); + assert_eq!(to_balance, 20100); + + unsafe { + let account_identities_mut = account_identities.cast_mut(); + wallet_ffi_free_account_identity(account_identities_mut); + wallet_ffi_free_account_identity(account_identities_mut.add(1)); + + let instruction_data = + std::slice::from_raw_parts_mut(instruction_words.cast_mut(), instruction_words_size); + drop(Box::from_raw(std::ptr::from_mut(instruction_data))); + + wallet_ffi_free_transaction_result(&raw mut transaction_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_transfer_generic_private() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); + let to: FfiBytes32 = ctx.ctx().existing_private_accounts()[1].into(); + let amount = 100_u128; + + let mut transaction_result = FfiTransactionResult::default(); + + let mut from_account_identity = FfiAccountIdentity::default(); + let mut to_account_identity = FfiAccountIdentity::default(); + + unsafe { + wallet_ffi_resolve_private_account(wallet_ffi_handle, from, &raw mut from_account_identity) + .unwrap(); + } + + unsafe { + wallet_ffi_resolve_private_account(wallet_ffi_handle, to, &raw mut to_account_identity) + .unwrap(); + } + + let ffi_accs = vec![from_account_identity, to_account_identity]; + let account_identities_size = ffi_accs.len(); + let account_identities = + Box::into_raw(ffi_accs.into_boxed_slice()) as *const FfiAccountIdentity; + + let instruction_data = + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount, + }) + .unwrap(); + let instruction_words_size = instruction_data.len(); + let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32; + + let program: ProgramWithDependencies = Program::authenticated_transfer_program().into(); + let program_with_dependencies: FfiProgramWithDependencies = program.into(); + + unsafe { + wallet_ffi_send_generic_private_transaction( + wallet_ffi_handle, + account_identities, + account_identities_size, + instruction_words, + instruction_words_size, + &raw const program_with_dependencies, + &raw mut transaction_result, + ) + .unwrap(); + } + + assert_eq!(transaction_result.secrets_size, 2); + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + let from_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const from, + false, + &raw mut out_balance, + ); + 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, + &raw const to, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(from_balance, 9900); + assert_eq!(to_balance, 20100); + + unsafe { + let account_identities_mut = account_identities.cast_mut(); + wallet_ffi_free_account_identity(account_identities_mut); + wallet_ffi_free_account_identity(account_identities_mut.add(1)); + + let instruction_data = + std::slice::from_raw_parts_mut(instruction_words.cast_mut(), instruction_words_size); + drop(Box::from_raw(std::ptr::from_mut(instruction_data))); + + wallet_ffi_free_transaction_result(&raw mut transaction_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} diff --git a/lez/wallet-ffi/Cargo.toml b/lez/wallet-ffi/Cargo.toml index 567ad27c..45e94ee6 100644 --- a/lez/wallet-ffi/Cargo.toml +++ b/lez/wallet-ffi/Cargo.toml @@ -19,6 +19,7 @@ sequencer_service_rpc = { workspace = true, features = ["client"] } tokio.workspace = true key_protocol.workspace = true serde_json.workspace = true +risc0-zkvm.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/lez/wallet-ffi/src/error.rs b/lez/wallet-ffi/src/error.rs index 206f24d8..c701cb6f 100644 --- a/lez/wallet-ffi/src/error.rs +++ b/lez/wallet-ffi/src/error.rs @@ -2,6 +2,8 @@ //! //! Uses numeric error codes with error messages printed to stderr. +use std::str::Utf8Error; + /// Error codes returned by FFI functions. #[repr(C)] #[must_use] @@ -41,10 +43,18 @@ pub enum WalletFfiError { InvalidTypeConversion = 15, /// Invalid Key value. InvalidKeyValue = 16, + /// Invalid program bytecode. + InvalidBytecode = 17, /// Internal error (catch-all). InternalError = 99, } +impl From for WalletFfiError { + fn from(_value: Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + impl WalletFfiError { /// Check if it's [`WalletFfiError::Success`] or panic. pub fn unwrap(self) { diff --git a/lez/wallet-ffi/src/generic_transaction.rs b/lez/wallet-ffi/src/generic_transaction.rs new file mode 100644 index 00000000..f8f344ad --- /dev/null +++ b/lez/wallet-ffi/src/generic_transaction.rs @@ -0,0 +1,469 @@ +use std::{ + collections::HashMap, + ffi::{c_char, CString}, +}; + +use lee::{privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program}; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + map_execution_error, + wallet::get_wallet, + FfiAccountIdentity, FfiBytes32, WalletHandle, +}; + +#[repr(C)] +pub struct FfiInstructionWords { + pub instruction_words: *mut u32, + pub instruction_words_size: usize, + pub error: WalletFfiError, +} + +impl FfiInstructionWords { + const fn from_err(error: WalletFfiError) -> Self { + Self { + instruction_words: std::ptr::null_mut(), + instruction_words_size: 0, + error, + } + } +} + +#[repr(C)] +/// Intended to be created manually. +pub struct FfiProgram { + pub elf_data: *const u8, + pub elf_size: usize, +} + +impl TryFrom<&FfiProgram> for Program { + type Error = WalletFfiError; + + fn try_from(value: &FfiProgram) -> Result { + let mut elf = Vec::with_capacity(value.elf_size); + + // Alignment will be different, we need to read elements one-by-one + for i in 0..value.elf_size { + elf.push(unsafe { *value.elf_data.add(i) }); + } + + Self::new(elf).map_err(|err| { + print_error(format!("Invalid program bytecode, err: {err}")); + WalletFfiError::InvalidBytecode + }) + } +} + +impl From for FfiProgram { + fn from(value: Program) -> Self { + let elf_clone = value.elf().to_vec(); + let elf_size = elf_clone.len(); + let elf_data = Box::into_raw(elf_clone.into_boxed_slice()) as *const u8; + + Self { elf_data, elf_size } + } +} + +#[repr(C)] +/// Intended to be created manually. +pub struct FfiProgramWithDependencies { + pub program: FfiProgram, + pub deps: *const FfiProgram, + pub deps_size: usize, +} + +impl TryFrom<&FfiProgramWithDependencies> for ProgramWithDependencies { + type Error = WalletFfiError; + + fn try_from(value: &FfiProgramWithDependencies) -> Result { + let mut program_map = HashMap::new(); + + let orig_program = (&value.program).try_into()?; + + // Alignment will be different, we need to read elements one-by-one + for i in 0..value.deps_size { + let program_dep: Program = unsafe { value.deps.add(i).as_ref() } + .ok_or(WalletFfiError::NullPointer)? + .try_into()?; + + program_map.insert(program_dep.id(), program_dep); + } + + Ok(Self { + program: orig_program, + dependencies: program_map, + }) + } +} + +impl From for FfiProgramWithDependencies { + fn from(value: ProgramWithDependencies) -> Self { + let ffi_program = value.program.into(); + + let ffi_deps: Vec = value + .dependencies + .into_values() + .map(Into::into) + .collect::>(); + + let deps_size = ffi_deps.len(); + let deps = Box::into_raw(ffi_deps.into_boxed_slice()) as *const FfiProgram; + + Self { + program: ffi_program, + deps, + deps_size, + } + } +} + +/// Result of a generic transaction operation. +#[repr(C)] +pub struct FfiTransactionResult { + // TODO: Replace with HashType FFI representation + /// Transaction hash (null-terminated string, or null on failure). + pub tx_hash: *mut c_char, + /// Whether the transaction succeeded. + pub success: bool, + pub secrets_data: *const FfiBytes32, + /// Public transactions have 0 secrets. + pub secrets_size: usize, +} + +impl Default for FfiTransactionResult { + fn default() -> Self { + Self { + tx_hash: std::ptr::null_mut(), + success: false, + secrets_data: std::ptr::null(), + secrets_size: 0, + } + } +} + +/// Serialize sequence of bytes into RISC0 readable words. +/// +/// # Parameters +/// - `input_instruction_data`: Valid pointer to a sequence of bytes +/// - `input_instruction_data_size`: Size of `input_instruction_data` +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `input_instruction_data` must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_serialization_helper( + input_instruction_data: *const u8, + input_instruction_data_size: usize, +) -> FfiInstructionWords { + if input_instruction_data.is_null() { + print_error("Null input pointer for instruction_data"); + return FfiInstructionWords::from_err(WalletFfiError::NullPointer); + } + + let input_slice = + unsafe { std::slice::from_raw_parts(input_instruction_data, input_instruction_data_size) }; + let res_vec_u32_with_prefix = match risc0_zkvm::serde::to_vec(input_slice).map_err(|err| { + print_error(format!( + "Failed to serialize input into words with err {err}" + )); + WalletFfiError::SerializationError + }) { + Ok(res) => res, + Err(err) => return FfiInstructionWords::from_err(err), + }; + + // The resulting vec contains len as prefix + let res_vec_u32 = res_vec_u32_with_prefix[1..].to_vec(); + + let res_len = res_vec_u32.len(); + let res_boxed = res_vec_u32.into_boxed_slice(); + let res_ptr = Box::into_raw(res_boxed).cast::(); + + FfiInstructionWords { + instruction_words: res_ptr, + instruction_words_size: res_len, + error: WalletFfiError::Success, + } +} + +/// Send generic public transaction. +/// +/// # Parameters +/// - `handle`: Valid pointer to wallet handle +/// - `account_identities`: Valid pointer to list of `FfiAccountIdentity` +/// - `instruction_words`: Valid pointer to instruction words +/// - `out_result`: Valid pointer to `FfiTransactionResult` +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid pointer +/// - `account_identities` must be a valid pointer +/// - `instruction_words` must be a valid pointer +/// - `out_result` must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_send_generic_public_transaction( + handle: *mut WalletHandle, + account_identities: *const FfiAccountIdentity, + account_identities_size: usize, + instruction_words: *const u32, + instruction_words_size: usize, + program_with_dependencies: *const FfiProgramWithDependencies, + out_result: *mut FfiTransactionResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_identities.is_null() { + print_error("Null input pointer for account identities list"); + return WalletFfiError::NullPointer; + } + + if instruction_words.is_null() { + print_error("Null input pointer for instruction data"); + return WalletFfiError::NullPointer; + } + + if out_result.is_null() { + print_error("Null output pointer return hash"); + 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 accounts_ffi = std::slice::from_raw_parts(account_identities, account_identities_size); + let instruction_data = std::slice::from_raw_parts(instruction_words, instruction_words_size); + + let mut accounts = Vec::with_capacity(account_identities_size); + + for ffi_acc in accounts_ffi { + match ffi_acc.try_into() { + Ok(v) => accounts.push(v), + Err(err) => { + print_error("Failed to convert FfiAccountIdentity into AccountIdentity"); + return err; + } + } + } + + let program = match unsafe { &*program_with_dependencies }.try_into() { + Ok(v) => v, + Err(err) => return err, + }; + + match block_on(wallet.send_pub_tx(accounts, instruction_data.to_vec(), &program)) { + Ok(tx_hash) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(std::ptr::null_mut(), std::ffi::CString::into_raw); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Err(e) => { + print_error(format!("Public send failed: {e:?}")); + unsafe { + (*out_result).tx_hash = std::ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} + +/// Send generic private transaction. +/// +/// # Parameters +/// - `handle`: Valid pointer to wallet handle +/// - `account_identities`: Valid pointer to list of `FfiAccountIdentity` +/// - `instruction_words`: Valid pointer to instruction words +/// - `out_result`: Valid pointer to `FfiTransactionResult` +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid pointer +/// - `account_identities` must be a valid pointer +/// - `instruction_words` must be a valid pointer +/// - `out_result` must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_send_generic_private_transaction( + handle: *mut WalletHandle, + account_identities: *const FfiAccountIdentity, + account_identities_size: usize, + instruction_words: *const u32, + instruction_words_size: usize, + program_with_dependencies: *const FfiProgramWithDependencies, + out_result: *mut FfiTransactionResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_identities.is_null() { + print_error("Null input pointer for account identities list"); + return WalletFfiError::NullPointer; + } + + if instruction_words.is_null() { + print_error("Null input pointer for instruction data"); + return WalletFfiError::NullPointer; + } + + if out_result.is_null() { + print_error("Null output pointer return hash"); + 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 accounts_ffi = std::slice::from_raw_parts(account_identities, account_identities_size); + let instruction_data = std::slice::from_raw_parts(instruction_words, instruction_words_size); + + let mut accounts = Vec::with_capacity(account_identities_size); + + for ffi_acc in accounts_ffi { + match ffi_acc.try_into() { + Ok(v) => accounts.push(v), + Err(err) => { + print_error("Failed to convert FfiAccountIdentity into AccountIdentity"); + return err; + } + } + } + + let program = match unsafe { &*program_with_dependencies }.try_into() { + Ok(v) => v, + Err(err) => return err, + }; + + match block_on(wallet.send_privacy_preserving_tx(accounts, instruction_data.to_vec(), &program)) + { + Ok((tx_hash, secrets)) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(std::ptr::null_mut(), std::ffi::CString::into_raw); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + + let secrets_size = secrets.len(); + let boxed_slice = secrets + .into_iter() + .map(Into::into) + .collect::>() + .into_boxed_slice(); + let secrets_data = Box::into_raw(boxed_slice) as *const FfiBytes32; + + (*out_result).secrets_size = secrets_size; + (*out_result).secrets_data = secrets_data; + } + WalletFfiError::Success + } + Err(e) => { + print_error(format!("Private send failed: {e:?}")); + unsafe { + *out_result = FfiTransactionResult::default(); + } + map_execution_error(e) + } + } +} + +/// Free a transaction result returned by `wallet_ffi_send_generic_public_transaction` or +/// `wallet_ffi_send_generic_private_transaction`. +/// +/// # Safety +/// The result must be either null or a valid result from a transaction function. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_transaction_result(result: *mut FfiTransactionResult) { + if result.is_null() { + return; + } + + unsafe { + let result = &*result; + if !result.tx_hash.is_null() { + drop(CString::from_raw(result.tx_hash)); + } + + if !result.secrets_data.is_null() { + let secrets = + std::slice::from_raw_parts_mut(result.secrets_data.cast_mut(), result.secrets_size); + drop(Box::from_raw(std::ptr::from_mut::<[FfiBytes32]>(secrets))); + } + } +} + +/// Free a instruction words returned by `wallet_ffi_serialization_helper`. +/// +/// # Safety +/// The result must be either null or a valid result from a serialization helper function. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_instruction_words(words: *mut FfiInstructionWords) { + if words.is_null() { + return; + } + + unsafe { + let words = &*words; + + if !words.instruction_words.is_null() { + let words = std::slice::from_raw_parts_mut( + words.instruction_words, + words.instruction_words_size, + ); + drop(Box::from_raw(std::ptr::from_mut::<[u32]>(words))); + } + } +} + +#[cfg(test)] +mod tests { + use lee::program::Program; + + use crate::generic_transaction::FfiProgram; + + #[test] + fn program_cast_consistency() { + let prog = Program::amm(); + + let first_5_bytes = prog.elf()[..5].to_vec(); + + let ffi_prog: FfiProgram = prog.into(); + + assert!(!ffi_prog.elf_data.is_null()); + + let mut ffi_first_5_bytes = vec![]; + for i in 0..5 { + ffi_first_5_bytes.push(unsafe { *ffi_prog.elf_data.add(i) }); + } + + assert_eq!(ffi_first_5_bytes, first_5_bytes); + } +} diff --git a/lez/wallet-ffi/src/keys.rs b/lez/wallet-ffi/src/keys.rs index 1eae1723..6a2c4d0b 100644 --- a/lez/wallet-ffi/src/keys.rs +++ b/lez/wallet-ffi/src/keys.rs @@ -1,13 +1,15 @@ //! Key retrieval functions. -use std::ptr; +use std::{ffi::CString, ptr}; use lee::{AccountId, PublicKey}; +use wallet::AccountIdentity; use crate::{ error::{print_error, WalletFfiError}, types::{FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, WalletHandle}, wallet::get_wallet, + FfiAccountIdentity, }; /// Get the public key for a public account. @@ -250,3 +252,153 @@ pub unsafe extern "C" fn wallet_ffi_account_id_from_base58( WalletFfiError::Success } + +/// Resolve public account. +/// +/// # Parameters +/// - `account_id`: 32 bytes of the public account ID +/// - `needs_sign`: whether the account needs signing +/// - `out_account_identity`: valid pointer, where output will be written +/// +/// # Returns +/// - `Success` on successful retrieval +/// +/// # Safety +/// - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_resolve_public_account( + account_id: FfiBytes32, + needs_sign: bool, + out_account_identity: *mut FfiAccountIdentity, +) -> WalletFfiError { + if out_account_identity.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let resolved_account = if needs_sign { + AccountIdentity::Public(account_id.into()) + } else { + AccountIdentity::PublicNoSign(account_id.into()) + }; + + unsafe { + *out_account_identity = resolved_account.into(); + } + + WalletFfiError::Success +} + +/// Resolve private account. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id`: 32 bytes of the public account ID +/// - `out_account_identity`: valid pointer, where output will be written +/// +/// # Returns +/// - `Success` on successful retrieval +/// - `InternalError` if failed to lock wallet +/// - `AccountNotFound` if the account is not found +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_resolve_private_account( + handle: *mut WalletHandle, + account_id: FfiBytes32, + out_account_identity: *mut FfiAccountIdentity, +) -> WalletFfiError { + if out_account_identity.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + 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; + } + }; + + let account_id = account_id.into(); + + let Some(resolved_account) = wallet.resolve_private_account(account_id) else { + print_error("Account not found"); + return WalletFfiError::AccountNotFound; + }; + + unsafe { + *out_account_identity = resolved_account.into(); + } + + WalletFfiError::Success +} + +/// Free account identity returned by `wallet_ffi_resolve_private_account` or +/// `wallet_ffi_resolve_public_account`. +/// +/// # Safety +/// The account must be either null or a valid account returned by +/// `wallet_ffi_resolve_private_account` or `wallet_ffi_resolve_public_account`. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_account_identity( + account_identity: *mut FfiAccountIdentity, +) { + if account_identity.is_null() { + return; + } + + unsafe { + let FfiAccountIdentity { + kind: _, + account_id: _, + key_path, + nullifier_secret_key: _, + nullifier_public_key: _, + viewing_public_key, + viewing_public_key_len, + identifier: _, + } = *account_identity; + + if !viewing_public_key.is_null() { + let slice = std::slice::from_raw_parts_mut( + viewing_public_key.cast_mut(), + viewing_public_key_len, + ); + drop(Box::from_raw(std::ptr::from_mut::<[u8]>(slice))); + } + + if !key_path.is_null() { + let key_path_cstring = CString::from_raw(key_path); + drop(key_path_cstring); + } + } +} + +#[cfg(test)] +mod tests { + use lee::AccountId; + use wallet::AccountIdentity; + + use crate::{keys::wallet_ffi_free_account_identity, FfiAccountIdentity}; + + #[test] + fn acc_identity_correct_free() { + let acc_identity = AccountIdentity::Public(AccountId::new([42; 32])); + let mut ffi_acc_identity: FfiAccountIdentity = acc_identity.into(); + + unsafe { + wallet_ffi_free_account_identity(&raw mut ffi_acc_identity); + } + + assert!(ffi_acc_identity.viewing_public_key.is_null()); + } +} diff --git a/lez/wallet-ffi/src/lib.rs b/lez/wallet-ffi/src/lib.rs index b28a548a..37e422cd 100644 --- a/lez/wallet-ffi/src/lib.rs +++ b/lez/wallet-ffi/src/lib.rs @@ -23,6 +23,7 @@ #![expect( clippy::undocumented_unsafe_blocks, clippy::multiple_unsafe_ops_per_block, + clippy::as_conversions, reason = "TODO: fix later" )] @@ -42,6 +43,7 @@ use crate::error::print_error; pub mod account; pub mod error; +pub mod generic_transaction; pub mod keys; pub mod pinata; pub mod sync; diff --git a/lez/wallet-ffi/src/types.rs b/lez/wallet-ffi/src/types.rs index 8c9e105d..ad366b91 100644 --- a/lez/wallet-ffi/src/types.rs +++ b/lez/wallet-ffi/src/types.rs @@ -1,9 +1,15 @@ //! C-compatible type definitions for the FFI layer. use core::slice; -use std::{ffi::c_char, ptr}; +use std::{ + ffi::{c_char, CString}, + ptr, + str::FromStr as _, +}; -use lee::Data; +use lee::{Data, SharedSecretKey}; +use lee_core::{encryption::MlKem768EncapsulationKey, NullifierPublicKey}; +use wallet::AccountIdentity; use crate::error::WalletFfiError; @@ -153,6 +159,12 @@ impl FfiBytes32 { } } +impl From for FfiBytes32 { + fn from(value: SharedSecretKey) -> Self { + Self { data: value.0 } + } +} + impl FfiPrivateAccountKeys { #[must_use] pub const fn npk(&self) -> lee_core::NullifierPublicKey { @@ -174,6 +186,50 @@ impl FfiPrivateAccountKeys { } } +/// Enumeration to represent kinds of `FfiAccountIdentity`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FfiAccountIdentityKind { + Public = 0, + PublicNoSign = 1, + PublicKeycard = 2, + PrivateOwned = 3, + PrivateForeign = 4, + PrivatePdaOwned = 5, + PrivatePdaForeign = 6, + PrivateShared = 7, + PrivatePdaShared = 8, +} + +/// Struct representing an account identity, given to `AccountManager` at intialization. +#[repr(C)] +pub struct FfiAccountIdentity { + pub kind: FfiAccountIdentityKind, + pub account_id: FfiBytes32, + /// C-compatible string. + pub key_path: *mut c_char, + pub nullifier_secret_key: FfiBytes32, + pub nullifier_public_key: FfiBytes32, + pub viewing_public_key: *const u8, + pub viewing_public_key_len: usize, + pub identifier: FfiU128, +} + +impl Default for FfiAccountIdentity { + fn default() -> Self { + Self { + kind: FfiAccountIdentityKind::Public, + account_id: FfiBytes32::default(), + key_path: std::ptr::null_mut(), + nullifier_secret_key: FfiBytes32::default(), + nullifier_public_key: FfiBytes32::default(), + viewing_public_key: std::ptr::null(), + viewing_public_key_len: 0, + identifier: FfiU128::default(), + } + } +} + impl From for FfiU128 { fn from(value: u128) -> Self { Self { @@ -194,6 +250,12 @@ impl From for FfiBytes32 { } } +impl From<[u8; 32]> for FfiBytes32 { + fn from(value: [u8; 32]) -> Self { + Self { data: value } + } +} + impl From for lee::AccountId { fn from(bytes: FfiBytes32) -> Self { Self::new(bytes.data) @@ -268,3 +330,384 @@ impl TryFrom<&FfiPublicAccountKey> for lee::PublicKey { Ok(public_key) } } + +impl From for FfiAccountIdentity { + fn from(value: AccountIdentity) -> Self { + match value { + AccountIdentity::Public(account_id) => Self { + kind: FfiAccountIdentityKind::Public, + account_id: account_id.into(), + ..Default::default() + }, + AccountIdentity::PublicNoSign(account_id) => Self { + kind: FfiAccountIdentityKind::PublicNoSign, + account_id: account_id.into(), + ..Default::default() + }, + AccountIdentity::PublicKeycard { + account_id, + key_path, + } => Self { + kind: FfiAccountIdentityKind::PublicKeycard, + account_id: account_id.into(), + key_path: CString::into_raw( + CString::from_str(&key_path).expect("key_path should be a valid string"), + ), + ..Default::default() + }, + AccountIdentity::PrivateOwned(account_id) => Self { + kind: FfiAccountIdentityKind::PrivateOwned, + account_id: account_id.into(), + ..Default::default() + }, + AccountIdentity::PrivateForeign { + npk, + vpk, + identifier, + } => { + let vpk_vec = vpk.to_bytes().to_vec(); + let vpk_len = vpk_vec.len(); + let vpk_data = if vpk_len > 0 { + let vpk_data_boxed = vpk_vec.into_boxed_slice(); + Box::into_raw(vpk_data_boxed) as *const u8 + } else { + ptr::null() + }; + + Self { + kind: FfiAccountIdentityKind::PrivateForeign, + nullifier_public_key: npk.0.into(), + viewing_public_key: vpk_data, + viewing_public_key_len: vpk_len, + identifier: identifier.into(), + ..Default::default() + } + } + AccountIdentity::PrivatePdaOwned(account_id) => Self { + kind: FfiAccountIdentityKind::PrivatePdaOwned, + account_id: account_id.into(), + ..Default::default() + }, + AccountIdentity::PrivatePdaForeign { + account_id, + npk, + vpk, + identifier, + } => { + let vpk_vec = vpk.to_bytes().to_vec(); + let vpk_len = vpk_vec.len(); + let vpk_data = if vpk_len > 0 { + let vpk_data_boxed = vpk_vec.into_boxed_slice(); + Box::into_raw(vpk_data_boxed) as *const u8 + } else { + ptr::null() + }; + + Self { + kind: FfiAccountIdentityKind::PrivatePdaForeign, + account_id: account_id.into(), + nullifier_public_key: npk.0.into(), + viewing_public_key: vpk_data, + viewing_public_key_len: vpk_len, + identifier: identifier.into(), + ..Default::default() + } + } + AccountIdentity::PrivateShared { + nsk, + npk, + vpk, + identifier, + } => { + let vpk_vec = vpk.to_bytes().to_vec(); + let vpk_len = vpk_vec.len(); + let vpk_data = if vpk_len > 0 { + let vpk_data_boxed = vpk_vec.into_boxed_slice(); + Box::into_raw(vpk_data_boxed) as *const u8 + } else { + ptr::null() + }; + + Self { + kind: FfiAccountIdentityKind::PrivateShared, + nullifier_secret_key: nsk.into(), + nullifier_public_key: npk.0.into(), + viewing_public_key: vpk_data, + viewing_public_key_len: vpk_len, + identifier: identifier.into(), + ..Default::default() + } + } + AccountIdentity::PrivatePdaShared { + account_id, + nsk, + npk, + vpk, + identifier, + } => { + let vpk_vec = vpk.to_bytes().to_vec(); + let vpk_len = vpk_vec.len(); + let vpk_data = if vpk_len > 0 { + let vpk_data_boxed = vpk_vec.into_boxed_slice(); + Box::into_raw(vpk_data_boxed) as *const u8 + } else { + ptr::null() + }; + + Self { + kind: FfiAccountIdentityKind::PrivatePdaShared, + account_id: account_id.into(), + nullifier_secret_key: nsk.into(), + nullifier_public_key: npk.0.into(), + viewing_public_key: vpk_data, + viewing_public_key_len: vpk_len, + identifier: identifier.into(), + ..Default::default() + } + } + } + } +} + +impl TryFrom<&FfiAccountIdentity> for AccountIdentity { + type Error = WalletFfiError; + + #[expect( + clippy::map_err_ignore, + reason = "`WalletFfiError` must be a trivial enum for FFI" + )] + fn try_from(value: &FfiAccountIdentity) -> Result { + match value.kind { + FfiAccountIdentityKind::Public => Ok(Self::Public(value.account_id.into())), + FfiAccountIdentityKind::PublicNoSign => Ok(Self::PublicNoSign(value.account_id.into())), + FfiAccountIdentityKind::PublicKeycard => { + let key_path = unsafe { CString::from_raw(value.key_path) } + .to_str()? + .to_owned(); + Ok(Self::PublicKeycard { + account_id: value.account_id.into(), + key_path, + }) + } + FfiAccountIdentityKind::PrivateOwned => Ok(Self::PrivateOwned(value.account_id.into())), + FfiAccountIdentityKind::PrivateForeign => { + let vpk = if value.viewing_public_key_len == 1184 { + let slice = unsafe { + slice::from_raw_parts( + value.viewing_public_key, + value.viewing_public_key_len, + ) + }; + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) + } else { + Err(WalletFfiError::InvalidKeyValue) + }?; + + Ok(Self::PrivateForeign { + npk: NullifierPublicKey(value.nullifier_public_key.data), + vpk, + identifier: value.identifier.into(), + }) + } + FfiAccountIdentityKind::PrivatePdaOwned => { + Ok(Self::PrivatePdaOwned(value.account_id.into())) + } + FfiAccountIdentityKind::PrivatePdaForeign => { + let vpk = if value.viewing_public_key_len == 1184 { + let slice = unsafe { + slice::from_raw_parts( + value.viewing_public_key, + value.viewing_public_key_len, + ) + }; + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) + } else { + Err(WalletFfiError::InvalidKeyValue) + }?; + + Ok(Self::PrivatePdaForeign { + account_id: value.account_id.into(), + npk: NullifierPublicKey(value.nullifier_public_key.data), + vpk, + identifier: value.identifier.into(), + }) + } + FfiAccountIdentityKind::PrivateShared => { + let vpk = if value.viewing_public_key_len == 1184 { + let slice = unsafe { + slice::from_raw_parts( + value.viewing_public_key, + value.viewing_public_key_len, + ) + }; + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) + } else { + Err(WalletFfiError::InvalidKeyValue) + }?; + + Ok(Self::PrivateShared { + nsk: value.nullifier_secret_key.data, + npk: NullifierPublicKey(value.nullifier_public_key.data), + vpk, + identifier: value.identifier.into(), + }) + } + FfiAccountIdentityKind::PrivatePdaShared => { + let vpk = if value.viewing_public_key_len == 1184 { + let slice = unsafe { + slice::from_raw_parts( + value.viewing_public_key, + value.viewing_public_key_len, + ) + }; + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) + } else { + Err(WalletFfiError::InvalidKeyValue) + }?; + + Ok(Self::PrivatePdaShared { + account_id: value.account_id.into(), + nsk: value.nullifier_secret_key.data, + npk: NullifierPublicKey(value.nullifier_public_key.data), + vpk, + identifier: value.identifier.into(), + }) + } + } + } +} + +#[cfg(test)] +mod tests { + use lee::{AccountId, PrivateKey, PublicKey}; + use lee_core::{encryption::ViewingPublicKey, program::PdaSeed, PrivateAccountKind}; + use wallet::AccountIdentity; + + use crate::{FfiAccountIdentity, FfiAccountIdentityKind}; + + #[test] + fn account_identity_roundtrip() { + let private_key = PrivateKey::try_new([42; 32]).unwrap(); + let public_key = PublicKey::new_from_private_key(&private_key); + let pub_acc_id = (&public_key).into(); + + let nsk = [43; 32]; + let vpk = ViewingPublicKey::from_seed(&[44; 32], &[54; 32]); + let npk = (&nsk).into(); + let identifier = u128::from_le_bytes([45; 16]); + + let private_reg_acc_id = + AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)); + let private_pda_acc_id = AccountId::for_private_account( + &npk, + &PrivateAccountKind::Pda { + program_id: [46; 8], + seed: PdaSeed::new([47; 32]), + identifier, + }, + ); + + let acc_identity_1 = AccountIdentity::Public(pub_acc_id); + let acc_identity_2 = AccountIdentity::PublicNoSign(pub_acc_id); + + let acc_identity_2_5 = AccountIdentity::PublicKeycard { + account_id: pub_acc_id, + key_path: "path/to/key".to_owned(), + }; + + let acc_identity_3 = AccountIdentity::PrivateOwned(private_reg_acc_id); + let acc_identity_4 = AccountIdentity::PrivateForeign { + npk, + vpk: vpk.clone(), + identifier, + }; + let acc_identity_5 = AccountIdentity::PrivatePdaOwned(private_pda_acc_id); + let acc_identity_6 = AccountIdentity::PrivatePdaForeign { + account_id: private_pda_acc_id, + npk, + vpk: vpk.clone(), + identifier, + }; + let acc_identity_7 = AccountIdentity::PrivateShared { + nsk, + npk, + vpk: vpk.clone(), + identifier, + }; + let acc_identity_8 = AccountIdentity::PrivatePdaShared { + account_id: private_pda_acc_id, + nsk, + npk, + vpk, + identifier, + }; + + let ffi_acc_identity_1: FfiAccountIdentity = acc_identity_1.clone().into(); + let ffi_acc_identity_2: FfiAccountIdentity = acc_identity_2.clone().into(); + let ffi_acc_identity_2_5: FfiAccountIdentity = acc_identity_2_5.clone().into(); + let ffi_acc_identity_3: FfiAccountIdentity = acc_identity_3.clone().into(); + let ffi_acc_identity_4: FfiAccountIdentity = acc_identity_4.clone().into(); + let ffi_acc_identity_5: FfiAccountIdentity = acc_identity_5.clone().into(); + let ffi_acc_identity_6: FfiAccountIdentity = acc_identity_6.clone().into(); + let ffi_acc_identity_7: FfiAccountIdentity = acc_identity_7.clone().into(); + let ffi_acc_identity_8: FfiAccountIdentity = acc_identity_8.clone().into(); + + assert_eq!(ffi_acc_identity_1.kind, FfiAccountIdentityKind::Public); + assert_eq!( + ffi_acc_identity_2.kind, + FfiAccountIdentityKind::PublicNoSign + ); + assert_eq!( + ffi_acc_identity_2_5.kind, + FfiAccountIdentityKind::PublicKeycard + ); + assert_eq!( + ffi_acc_identity_3.kind, + FfiAccountIdentityKind::PrivateOwned + ); + assert_eq!( + ffi_acc_identity_4.kind, + FfiAccountIdentityKind::PrivateForeign + ); + assert_eq!( + ffi_acc_identity_5.kind, + FfiAccountIdentityKind::PrivatePdaOwned + ); + assert_eq!( + ffi_acc_identity_6.kind, + FfiAccountIdentityKind::PrivatePdaForeign + ); + assert_eq!( + ffi_acc_identity_7.kind, + FfiAccountIdentityKind::PrivateShared + ); + assert_eq!( + ffi_acc_identity_8.kind, + FfiAccountIdentityKind::PrivatePdaShared + ); + + let acc_identity_res_1: AccountIdentity = (&ffi_acc_identity_1).try_into().unwrap(); + let acc_identity_res_2: AccountIdentity = (&ffi_acc_identity_2).try_into().unwrap(); + let acc_identity_res_2_5: AccountIdentity = (&ffi_acc_identity_2_5).try_into().unwrap(); + let acc_identity_res_3: AccountIdentity = (&ffi_acc_identity_3).try_into().unwrap(); + let acc_identity_res_4: AccountIdentity = (&ffi_acc_identity_4).try_into().unwrap(); + let acc_identity_res_5: AccountIdentity = (&ffi_acc_identity_5).try_into().unwrap(); + let acc_identity_res_6: AccountIdentity = (&ffi_acc_identity_6).try_into().unwrap(); + let acc_identity_res_7: AccountIdentity = (&ffi_acc_identity_7).try_into().unwrap(); + let acc_identity_res_8: AccountIdentity = (&ffi_acc_identity_8).try_into().unwrap(); + + assert_eq!(acc_identity_res_1, acc_identity_1); + assert_eq!(acc_identity_res_2, acc_identity_2); + assert_eq!(acc_identity_res_2_5, acc_identity_2_5); + assert_eq!(acc_identity_res_3, acc_identity_3); + assert_eq!(acc_identity_res_4, acc_identity_4); + assert_eq!(acc_identity_res_5, acc_identity_5); + assert_eq!(acc_identity_res_6, acc_identity_6); + assert_eq!(acc_identity_res_7, acc_identity_7); + assert_eq!(acc_identity_res_8, acc_identity_8); + } +} diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index 512b3b96..a8506a8a 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -103,12 +103,31 @@ typedef enum WalletFfiError { * Invalid Key value. */ INVALID_KEY_VALUE = 16, + /** + * Invalid program bytecode. + */ + INVALID_BYTECODE = 17, /** * Internal error (catch-all). */ INTERNAL_ERROR = 99, } WalletFfiError; +/** + * Enumeration to represent kinds of `FfiAccountIdentity`. + */ +typedef enum FfiAccountIdentityKind { + PUBLIC = 0, + PUBLIC_NO_SIGN = 1, + PUBLIC_KEYCARD = 2, + PRIVATE_OWNED = 3, + PRIVATE_FOREIGN = 4, + PRIVATE_PDA_OWNED = 5, + PRIVATE_PDA_FOREIGN = 6, + PRIVATE_SHARED = 7, + PRIVATE_PDA_SHARED = 8, +} FfiAccountIdentityKind; + /** * Opaque pointer to the Wallet instance. * @@ -200,6 +219,65 @@ typedef struct FfiAccount { struct FfiU128 nonce; } FfiAccount; +typedef struct FfiInstructionWords { + uint32_t *instruction_words; + uintptr_t instruction_words_size; + enum WalletFfiError error; +} FfiInstructionWords; + +/** + * Struct representing an account identity, given to `AccountManager` at intialization. + */ +typedef struct FfiAccountIdentity { + enum FfiAccountIdentityKind kind; + struct FfiBytes32 account_id; + /** + * C-compatible string. + */ + char *key_path; + struct FfiBytes32 nullifier_secret_key; + struct FfiBytes32 nullifier_public_key; + const uint8_t *viewing_public_key; + uintptr_t viewing_public_key_len; + struct FfiU128 identifier; +} FfiAccountIdentity; + +/** + * Intended to be created manually. + */ +typedef struct FfiProgram { + const uint8_t *elf_data; + uintptr_t elf_size; +} FfiProgram; + +/** + * Intended to be created manually. + */ +typedef struct FfiProgramWithDependencies { + struct FfiProgram program; + const struct FfiProgram *deps; + uintptr_t deps_size; +} FfiProgramWithDependencies; + +/** + * Result of a generic transaction operation. + */ +typedef struct FfiTransactionResult { + /** + * Transaction hash (null-terminated string, or null on failure). + */ + char *tx_hash; + /** + * Whether the transaction succeeded. + */ + bool success; + const struct FfiBytes32 *secrets_data; + /** + * Public transactions have 0 secrets. + */ + uintptr_t secrets_size; +} FfiTransactionResult; + /** * Public key info for a public account. */ @@ -454,6 +532,94 @@ enum WalletFfiError wallet_ffi_import_private_account(struct WalletHandle *handl const struct FfiU128 *identifier, const char *account_state_json); +/** + * Serialize sequence of bytes into RISC0 readable words. + * + * # Parameters + * - `input_instruction_data`: Valid pointer to a sequence of bytes + * - `input_instruction_data_size`: Size of `input_instruction_data` + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `input_instruction_data` must be a valid pointer + */ +struct FfiInstructionWords wallet_ffi_serialization_helper(const uint8_t *input_instruction_data, + uintptr_t input_instruction_data_size); + +/** + * Send generic public transaction. + * + * # Parameters + * - `handle`: Valid pointer to wallet handle + * - `account_identities`: Valid pointer to list of `FfiAccountIdentity` + * - `instruction_words`: Valid pointer to instruction words + * - `out_result`: Valid pointer to `FfiTransactionResult` + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `handle` must be a valid pointer + * - `account_identities` must be a valid pointer + * - `instruction_words` must be a valid pointer + * - `out_result` must be a valid pointer + */ +enum WalletFfiError wallet_ffi_send_generic_public_transaction(struct WalletHandle *handle, + const struct FfiAccountIdentity *account_identities, + uintptr_t account_identities_size, + const uint32_t *instruction_words, + uintptr_t instruction_words_size, + const struct FfiProgramWithDependencies *program_with_dependencies, + struct FfiTransactionResult *out_result); + +/** + * Send generic private transaction. + * + * # Parameters + * - `handle`: Valid pointer to wallet handle + * - `account_identities`: Valid pointer to list of `FfiAccountIdentity` + * - `instruction_words`: Valid pointer to instruction words + * - `out_result`: Valid pointer to `FfiTransactionResult` + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `handle` must be a valid pointer + * - `account_identities` must be a valid pointer + * - `instruction_words` must be a valid pointer + * - `out_result` must be a valid pointer + */ +enum WalletFfiError wallet_ffi_send_generic_private_transaction(struct WalletHandle *handle, + const struct FfiAccountIdentity *account_identities, + uintptr_t account_identities_size, + const uint32_t *instruction_words, + uintptr_t instruction_words_size, + const struct FfiProgramWithDependencies *program_with_dependencies, + struct FfiTransactionResult *out_result); + +/** + * Free a transaction result returned by `wallet_ffi_send_generic_public_transaction` or + * `wallet_ffi_send_generic_private_transaction`. + * + * # Safety + * The result must be either null or a valid result from a transaction function. + */ +void wallet_ffi_free_transaction_result(struct FfiTransactionResult *result); + +/** + * Free a instruction words returned by `wallet_ffi_serialization_helper`. + * + * # Safety + * The result must be either null or a valid result from a serialization helper function. + */ +void wallet_ffi_free_instruction_words(struct FfiInstructionWords *words); + /** * Get the public key for a public account. * @@ -552,6 +718,55 @@ char *wallet_ffi_account_id_to_base58(const struct FfiBytes32 *account_id); enum WalletFfiError wallet_ffi_account_id_from_base58(const char *base58_str, struct FfiBytes32 *out_account_id); +/** + * Resolve public account. + * + * # Parameters + * - `account_id`: 32 bytes of the public account ID + * - `needs_sign`: whether the account needs signing + * - `out_account_identity`: valid pointer, where output will be written + * + * # Returns + * - `Success` on successful retrieval + * + * # Safety + * - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct + */ +enum WalletFfiError wallet_ffi_resolve_public_account(struct FfiBytes32 account_id, + bool needs_sign, + struct FfiAccountIdentity *out_account_identity); + +/** + * Resolve private account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id`: 32 bytes of the public account ID + * - `out_account_identity`: valid pointer, where output will be written + * + * # Returns + * - `Success` on successful retrieval + * - `InternalError` if failed to lock wallet + * - `AccountNotFound` if the account is not found + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct + */ +enum WalletFfiError wallet_ffi_resolve_private_account(struct WalletHandle *handle, + struct FfiBytes32 account_id, + struct FfiAccountIdentity *out_account_identity); + +/** + * Free account identity returned by `wallet_ffi_resolve_private_account` or + * `wallet_ffi_resolve_public_account`. + * + * # Safety + * The account must be either null or a valid account returned by + * `wallet_ffi_resolve_private_account` or `wallet_ffi_resolve_public_account`. + */ +void wallet_ffi_free_account_identity(struct FfiAccountIdentity *account_identity); + /** * Claim a pinata reward using a public transaction. * diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index 7ec9b5c2..ce9d1833 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -1,3 +1,5 @@ +use core::fmt; + use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use keycard_wallet::{KeycardWallet, python_path}; @@ -11,7 +13,7 @@ use lee_core::{ use crate::{ExecutionFailureKind, WalletCore}; -#[derive(Clone, Debug)] +#[derive(Clone, PartialEq, Eq)] pub enum AccountIdentity { Public(AccountId), /// A public account without signing. Would not try to sign, even if account is owned. @@ -58,6 +60,73 @@ pub enum AccountIdentity { }, } +impl fmt::Debug for AccountIdentity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Public(id) => f.debug_tuple("Public").field(id).finish(), + Self::PublicNoSign(id) => f.debug_tuple("PublicNoSign").field(id).finish(), + Self::PublicKeycard { + account_id, + key_path: _, + } => f + .debug_struct("PublicKeycard") + .field("account_id", account_id) + .field("key_path", &"") + .finish(), + Self::PrivateOwned(id) => f.debug_tuple("PrivateOwned").field(id).finish(), + Self::PrivateForeign { + npk, + vpk, + identifier, + } => f + .debug_struct("PrivateForeign") + .field("npk", npk) + .field("vpk", vpk) + .field("identifier", identifier) + .finish(), + Self::PrivatePdaOwned(id) => f.debug_tuple("PrivatePdaOwned").field(id).finish(), + Self::PrivatePdaForeign { + account_id, + npk, + vpk, + identifier, + } => f + .debug_struct("PrivatePdaForeign") + .field("account_id", account_id) + .field("npk", npk) + .field("vpk", vpk) + .field("identifier", identifier) + .finish(), + Self::PrivateShared { + npk, + vpk, + identifier, + .. + } => f + .debug_struct("PrivateShared") + .field("nsk", &"") + .field("npk", npk) + .field("vpk", vpk) + .field("identifier", identifier) + .finish(), + Self::PrivatePdaShared { + account_id, + npk, + vpk, + identifier, + .. + } => f + .debug_struct("PrivatePdaShared") + .field("account_id", account_id) + .field("nsk", &"") + .field("npk", npk) + .field("vpk", vpk) + .field("identifier", identifier) + .finish(), + } + } +} + impl AccountIdentity { #[must_use] /// Note: `PublicNoSign` still counts as public, the variant just suppresses the signing-key