diff --git a/wallet-ffi/src/error.rs b/wallet-ffi/src/error.rs index a8c345b5..899b0deb 100644 --- a/wallet-ffi/src/error.rs +++ b/wallet-ffi/src/error.rs @@ -40,6 +40,8 @@ pub enum WalletFfiError { InvalidTypeConversion = 15, /// Invalid Key value. InvalidKeyValue = 16, + /// Invalid argument value. + InvalidArgument = 17, /// Internal error (catch-all). InternalError = 99, } diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 739832ae..c2689f4b 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -94,6 +94,107 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( } } +/// Send an arbitrary public transaction to a program. +/// +/// Builds a `PublicTransaction` from the given program, accounts, and instruction data, +/// signs it with the signer's key, and submits to the sequencer. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `program_id`: 32-byte program ID +/// - `accounts`: Pointer to array of 32-byte account IDs +/// - `num_accounts`: Number of accounts in the array +/// - `instruction_data`: Pointer to raw instruction bytes (Vec serialized as LE bytes) +/// - `instruction_len`: Length of instruction data in bytes (must be multiple of 4) +/// - `signer`: Signer account ID (must be owned by this wallet) +/// - `out_result`: Output pointer for transfer result +/// +/// # Safety +/// All pointers must be valid. `instruction_len` must be a multiple of 4. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_send_public_transaction( + handle: *mut WalletHandle, + program_id: *const FfiBytes32, + accounts: *const FfiBytes32, + num_accounts: usize, + instruction_data: *const u8, + instruction_len: usize, + signer: *const FfiBytes32, + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if program_id.is_null() + || accounts.is_null() + || instruction_data.is_null() + || signer.is_null() + || out_result.is_null() + { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + if instruction_len % 4 != 0 { + print_error("instruction_len must be a multiple of 4"); + return WalletFfiError::InvalidArgument; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let pid_bytes = unsafe { (*program_id).data }; + let mut pid: nssa_core::program::ProgramId = [0u32; 8]; + for i in 0..8 { + pid[i] = u32::from_le_bytes([ + pid_bytes[i * 4], + pid_bytes[i * 4 + 1], + pid_bytes[i * 4 + 2], + pid_bytes[i * 4 + 3], + ]); + } + let signer_id = AccountId::new(unsafe { (*signer).data }); + + let account_ids: Vec = (0..num_accounts) + .map(|i| AccountId::new(unsafe { (*accounts.add(i)).data })) + .collect(); + + // Convert raw bytes to Vec (LE) + let instr_slice = unsafe { std::slice::from_raw_parts(instruction_data, instruction_len) }; + let instr_u32: Vec = instr_slice + .chunks_exact(4) + .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])) + .collect(); + + match block_on(wallet.send_public_transaction(pid, account_ids, instr_u32, signer_id)) { + Ok(tx_hash) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(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!("Transaction failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} + /// Send a shielded token transfer. /// /// Transfers tokens from a public account to a private account. diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 2665cd40..e8c88846 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -103,6 +103,10 @@ typedef enum WalletFfiError { * Invalid Key value. */ INVALID_KEY_VALUE = 16, + /** + * Invalid argument value. + */ + INVALID_ARGUMENT = 17, /** * Internal error (catch-all). */ @@ -676,6 +680,34 @@ enum WalletFfiError wallet_ffi_transfer_public(struct WalletHandle *handle, const uint8_t (*amount)[16], struct FfiTransferResult *out_result); +/** + * Send an arbitrary public transaction to a program. + * + * Builds a `PublicTransaction` from the given program, accounts, and instruction data, + * signs it with the signer's key, and submits to the sequencer. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `program_id`: 32-byte program ID + * - `accounts`: Pointer to array of 32-byte account IDs + * - `num_accounts`: Number of accounts in the array + * - `instruction_data`: Pointer to raw instruction bytes (Vec serialized as LE bytes) + * - `instruction_len`: Length of instruction data in bytes (must be multiple of 4) + * - `signer`: Signer account ID (must be owned by this wallet) + * - `out_result`: Output pointer for transfer result + * + * # Safety + * All pointers must be valid. `instruction_len` must be a multiple of 4. + */ +enum WalletFfiError wallet_ffi_send_public_transaction(struct WalletHandle *handle, + const struct FfiBytes32 *program_id, + const struct FfiBytes32 *accounts, + uintptr_t num_accounts, + const uint8_t *instruction_data, + uintptr_t instruction_len, + const struct FfiBytes32 *signer, + struct FfiTransferResult *out_result); + /** * Send a shielded token transfer. * diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 63ea8611..cfebbf53 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -290,6 +290,41 @@ impl WalletCore { .get_pub_account_signing_key(account_id) } + /// Send an arbitrary public transaction to a program. + pub async fn send_public_transaction( + &self, + program_id: nssa_core::program::ProgramId, + account_ids: Vec, + instruction_data: InstructionData, + signer: AccountId, + ) -> Result { + let nonces = self + .get_accounts_nonces(vec![signer]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let message = nssa::public_transaction::Message::new_preserialized( + program_id, + account_ids, + nonces, + instruction_data, + ); + + let signing_key = self.storage.user_data.get_pub_account_signing_key(signer); + let Some(signing_key) = signing_key else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + #[must_use] pub fn get_account_private(&self, account_id: AccountId) -> Option { self.storage