From d8537ea3f031f20bc546572f85b69d4246cbbbdc Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 11 Feb 2026 18:34:45 -0300 Subject: [PATCH] add wallet ffi auth-transfer private method --- integration_tests/tests/wallet_ffi.rs | 90 ++++++++++++++++++++++- wallet-ffi/src/transfer.rs | 100 +++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 4 deletions(-) diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 479beae2..548822f4 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -117,6 +117,14 @@ unsafe extern "C" { out_result: *mut FfiTransferResult, ) -> error::WalletFfiError; + fn wallet_ffi_transfer_private( + 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( @@ -839,7 +847,7 @@ 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 from: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into(); let (to, to_keys) = unsafe { let mut out_account_id = FfiBytes32::default(); let mut out_keys = FfiPrivateAccountKeys::default(); @@ -915,7 +923,7 @@ fn test_wallet_ffi_transfer_deshielded() -> 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_PRIVATE.parse::().unwrap()).into(); + let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into(); let to = FfiBytes32::from_bytes([37; 32]); let amount: [u8; 16] = 100u128.to_le_bytes(); @@ -972,3 +980,81 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { Ok(()) } + +#[test] +fn test_wallet_ffi_transfer_private() -> 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 = (&ctx.ctx().existing_private_accounts()[0]).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_private( + 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, + false, + (&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 9dd5e73c..003488d0 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -104,7 +104,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( /// Send a shielded token transfer. /// -/// Transfers tokens from one private account to another on the network. +/// Transfers tokens from a public account to a private account. /// /// # Parameters /// - `handle`: Valid wallet handle @@ -201,7 +201,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded( /// Send a deshielded token transfer. /// -/// Transfers tokens from one private account to another on the network. +/// Transfers tokens from a private account to a public account. /// /// # Parameters /// - `handle`: Valid wallet handle @@ -287,6 +287,102 @@ pub unsafe extern "C" fn wallet_ffi_transfer_deshielded( } } +/// Send a private token transfer. +/// +/// Transfers tokens from a private account to another private account. +/// +/// # 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_private( + 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_private_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