diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index e934d1ea..16e96a7a 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -2,7 +2,7 @@ use std::{ collections::HashSet, ffi::{CStr, CString, c_char}, io::Write, - path::{Path, PathBuf}, + path::Path, time::Duration, }; @@ -101,6 +101,14 @@ unsafe extern "C" { out_result: *mut FfiTransferResult, ) -> error::WalletFfiError; + fn wallet_ffi_transfer_shielded( + handle: *mut WalletHandle, + from: *const FfiBytes32, + to_keys: *const FfiPrivateAccountKeys, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult); fn wallet_ffi_register_public_account( @@ -732,6 +740,7 @@ fn test_wallet_ffi_init_private_account_auth_transfer() -> Result<()> { 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, (&mut current_height) as *mut u64); @@ -816,3 +825,79 @@ fn test_wallet_ffi_transfer_public() -> Result<()> { Ok(()) } + +#[test] +fn test_wallet_ffi_transfer_shielded() -> Result<()> { + let ctx = BlockingTestContext::new().unwrap(); + let home = tempfile::tempdir().unwrap(); + let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path()); + let from: FfiBytes32 = (&ACC_SENDER.parse::().unwrap()).into(); + let (to, to_keys) = unsafe { + let mut out_account_id = FfiBytes32::default(); + let mut out_keys = FfiPrivateAccountKeys::default(); + wallet_ffi_create_account_private( + wallet_ffi_handle, + (&mut out_account_id) as *mut FfiBytes32, + ); + wallet_ffi_get_private_account_keys( + wallet_ffi_handle, + (&out_account_id) as *const FfiBytes32, + (&mut out_keys) as *mut FfiPrivateAccountKeys, + ); + (out_account_id, out_keys) + }; + let amount: [u8; 16] = 100u128.to_le_bytes(); + + let mut transfer_result = FfiTransferResult::default(); + unsafe { + wallet_ffi_transfer_shielded( + wallet_ffi_handle, + (&from) as *const FfiBytes32, + (&to_keys) as *const FfiPrivateAccountKeys, + (&amount) as *const [u8; 16], + (&mut transfer_result) as *mut FfiTransferResult, + ); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, (&mut current_height) as *mut u64); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height); + }; + + let from_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + (&from) as *const FfiBytes32, + true, + (&mut out_balance) as *mut [u8; 16], + ); + u128::from_le_bytes(out_balance) + }; + + let to_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + (&to) as *const FfiBytes32, + false, + (&mut out_balance) as *mut [u8; 16], + ); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(from_balance, 9900); + assert_eq!(to_balance, 100); + + unsafe { + wallet_ffi_free_transfer_result((&mut transfer_result) as *mut FfiTransferResult); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 030841c2..b9bc15b4 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -11,6 +11,7 @@ use crate::{ error::{print_error, WalletFfiError}, types::{FfiBytes32, FfiTransferResult, WalletHandle}, wallet::get_wallet, + FfiPrivateAccountKeys, }; /// Send a public token transfer. @@ -101,6 +102,103 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( } } +/// Send a token transfer shielded transfer. +/// +/// Transfers tokens from one private account to another on the network. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `from`: Source account ID (must be owned by this wallet) +/// - `to_keys`: Destination account keys +/// - `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_keys` must be a valid pointer to a `FfiPrivateAccountKeys` 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_shielded( + handle: *mut WalletHandle, + from: *const FfiBytes32, + to_keys: *const FfiPrivateAccountKeys, + 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_keys.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_npk = (*to_keys).npk(); + let to_ipk = match (*to_keys).ivk() { + Ok(ipk) => ipk, + Err(e) => { + print_error("Invalid viewing key"); + return e; + } + }; + let amount = u128::from_le_bytes(unsafe { *amount }); + + let transfer = NativeTokenTransfer(&wallet); + + match block_on( + transfer.send_shielded_transfer_to_outer_account(from_id, to_npk, to_ipk, amount), + ) { + Ok(Ok((response, _shared_key))) => { + 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 @@ -179,7 +277,6 @@ pub unsafe extern "C" fn wallet_ffi_register_public_account( } } - /// Register a private account on the network. /// /// This initializes a private account. The account must be