diff --git a/wallet-ffi/src/lib.rs b/wallet-ffi/src/lib.rs index e7f2ce98..99a0ee98 100644 --- a/wallet-ffi/src/lib.rs +++ b/wallet-ffi/src/lib.rs @@ -23,6 +23,7 @@ pub mod account; pub mod error; pub mod keys; +pub mod pinata; pub mod sync; pub mod transfer; pub mod types; diff --git a/wallet-ffi/src/pinata.rs b/wallet-ffi/src/pinata.rs new file mode 100644 index 00000000..2cf2fc84 --- /dev/null +++ b/wallet-ffi/src/pinata.rs @@ -0,0 +1,308 @@ +//! Pinata program interaction functions. + +use std::{ffi::CString, ptr, slice}; + +use common::error::ExecutionFailureKind; +use nssa::AccountId; +use nssa_core::MembershipProof; +use wallet::program_facades::pinata::Pinata; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + types::{FfiBytes32, FfiTransferResult, WalletHandle}, + wallet::get_wallet, +}; + +/// Claim a pinata reward using a public transaction. +/// +/// Sends a public claim transaction to the pinata program. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `pinata_account_id`: The pinata program account ID +/// - `winner_account_id`: The recipient account ID +/// - `solution`: The solution value as little-endian [u8; 16] +/// - `out_result`: Output pointer for the transaction result +/// +/// # Returns +/// - `Success` if the claim transaction 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` +/// - `pinata_account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `winner_account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `solution` 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_claim_pinata( + handle: *mut WalletHandle, + pinata_account_id: *const FfiBytes32, + winner_account_id: *const FfiBytes32, + solution: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if pinata_account_id.is_null() + || winner_account_id.is_null() + || solution.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 pinata_id = AccountId::new(unsafe { (*pinata_account_id).data }); + let winner_id = AccountId::new(unsafe { (*winner_account_id).data }); + let solution = u128::from_le_bytes(unsafe { *solution }); + + let pinata = Pinata(&wallet); + + match block_on(pinata.claim(pinata_id, winner_id, solution)) { + Ok(Ok(response)) => { + let tx_hash = CString::new(response.tx_hash.to_string()) + .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!("Pinata claim failed: {:?}", e)); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + Err(e) => e, + } +} + +/// Claim a pinata reward using a private transaction for an already-initialized owned account. +/// +/// Sends a privacy-preserving claim transaction for a winner account that already has +/// an on-chain commitment (i.e. was previously initialized). +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `pinata_account_id`: The pinata program account ID +/// - `winner_account_id`: The recipient private account ID (must be owned by this wallet) +/// - `solution`: The solution value as little-endian [u8; 16] +/// - `winner_proof_index`: Leaf index in the commitment tree for the membership proof +/// - `winner_proof_siblings`: Pointer to an array of 32-byte sibling hashes +/// - `winner_proof_siblings_len`: Number of sibling hashes in the array +/// - `out_result`: Output pointer for the transaction result +/// +/// # Returns +/// - `Success` if the claim transaction 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` +/// - `pinata_account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `winner_account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `solution` must be a valid pointer to a `[u8; 16]` array +/// - `winner_proof_siblings` must be a valid pointer to an array of `winner_proof_siblings_len` +/// elements of `[u8; 32]`, or null if `winner_proof_siblings_len` is 0 +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_claim_pinata_private_owned_already_initialized( + handle: *mut WalletHandle, + pinata_account_id: *const FfiBytes32, + winner_account_id: *const FfiBytes32, + solution: *const [u8; 16], + winner_proof_index: usize, + winner_proof_siblings: *const [u8; 32], + winner_proof_siblings_len: usize, + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if pinata_account_id.is_null() + || winner_account_id.is_null() + || solution.is_null() + || out_result.is_null() + { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + if winner_proof_siblings_len > 0 && winner_proof_siblings.is_null() { + print_error("Null proof siblings pointer with non-zero length"); + 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 pinata_id = AccountId::new(unsafe { (*pinata_account_id).data }); + let winner_id = AccountId::new(unsafe { (*winner_account_id).data }); + let solution = u128::from_le_bytes(unsafe { *solution }); + + let siblings = if winner_proof_siblings_len > 0 { + unsafe { slice::from_raw_parts(winner_proof_siblings, winner_proof_siblings_len).to_vec() } + } else { + vec![] + }; + let proof: MembershipProof = (winner_proof_index, siblings); + + let pinata = Pinata(&wallet); + + match block_on( + pinata + .claim_private_owned_account_already_initialized(pinata_id, winner_id, solution, proof), + ) { + Ok(Ok((response, _shared_key))) => { + let tx_hash = CString::new(response.tx_hash.to_string()) + .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!( + "Pinata private claim (already initialized) failed: {:?}", + e + )); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + Err(e) => e, + } +} + +/// Claim a pinata reward using a private transaction for a not-yet-initialized owned account. +/// +/// Sends a privacy-preserving claim transaction for a winner account that has not yet +/// been committed on-chain (i.e. is being initialized as part of this claim). +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `pinata_account_id`: The pinata program account ID +/// - `winner_account_id`: The recipient private account ID (must be owned by this wallet) +/// - `solution`: The solution value as little-endian [u8; 16] +/// - `out_result`: Output pointer for the transaction result +/// +/// # Returns +/// - `Success` if the claim transaction 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` +/// - `pinata_account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `winner_account_id` must be a valid pointer to a `FfiBytes32` struct +/// - `solution` 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_claim_pinata_private_owned_not_initialized( + handle: *mut WalletHandle, + pinata_account_id: *const FfiBytes32, + winner_account_id: *const FfiBytes32, + solution: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if pinata_account_id.is_null() + || winner_account_id.is_null() + || solution.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 pinata_id = AccountId::new(unsafe { (*pinata_account_id).data }); + let winner_id = AccountId::new(unsafe { (*winner_account_id).data }); + let solution = u128::from_le_bytes(unsafe { *solution }); + + let pinata = Pinata(&wallet); + + match block_on(pinata.claim_private_owned_account(pinata_id, winner_id, solution)) { + Ok(Ok((response, _shared_key))) => { + let tx_hash = CString::new(response.tx_hash.to_string()) + .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!( + "Pinata private claim (not initialized) failed: {:?}", + e + )); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + Err(e) => e, + } +} + +fn map_execution_error(e: ExecutionFailureKind) -> WalletFfiError { + match e { + ExecutionFailureKind::InsufficientFundsError => WalletFfiError::InsufficientFunds, + ExecutionFailureKind::KeyNotFoundError => WalletFfiError::KeyNotFound, + ExecutionFailureKind::SequencerError => WalletFfiError::NetworkError, + ExecutionFailureKind::SequencerClientError(_) => WalletFfiError::NetworkError, + _ => WalletFfiError::InternalError, + } +} diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 55f37cff..6b191506 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -475,6 +475,112 @@ 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); +/** + * Claim a pinata reward using a public transaction. + * + * Sends a public claim transaction to the pinata program. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `pinata_account_id`: The pinata program account ID + * - `winner_account_id`: The recipient account ID + * - `solution`: The solution value as little-endian [u8; 16] + * - `out_result`: Output pointer for the transaction result + * + * # Returns + * - `Success` if the claim transaction 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` + * - `pinata_account_id` must be a valid pointer to a `FfiBytes32` struct + * - `winner_account_id` must be a valid pointer to a `FfiBytes32` struct + * - `solution` 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_claim_pinata(struct WalletHandle *handle, + const struct FfiBytes32 *pinata_account_id, + const struct FfiBytes32 *winner_account_id, + const uint8_t (*solution)[16], + struct FfiTransferResult *out_result); + +/** + * Claim a pinata reward using a private transaction for an already-initialized owned account. + * + * Sends a privacy-preserving claim transaction for a winner account that already has + * an on-chain commitment (i.e. was previously initialized). + * + * # Parameters + * - `handle`: Valid wallet handle + * - `pinata_account_id`: The pinata program account ID + * - `winner_account_id`: The recipient private account ID (must be owned by this wallet) + * - `solution`: The solution value as little-endian [u8; 16] + * - `winner_proof_index`: Leaf index in the commitment tree for the membership proof + * - `winner_proof_siblings`: Pointer to an array of 32-byte sibling hashes + * - `winner_proof_siblings_len`: Number of sibling hashes in the array + * - `out_result`: Output pointer for the transaction result + * + * # Returns + * - `Success` if the claim transaction 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` + * - `pinata_account_id` must be a valid pointer to a `FfiBytes32` struct + * - `winner_account_id` must be a valid pointer to a `FfiBytes32` struct + * - `solution` must be a valid pointer to a `[u8; 16]` array + * - `winner_proof_siblings` must be a valid pointer to an array of `winner_proof_siblings_len` + * elements of `[u8; 32]`, or null if `winner_proof_siblings_len` is 0 + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_claim_pinata_private_owned_already_initialized(struct WalletHandle *handle, + const struct FfiBytes32 *pinata_account_id, + const struct FfiBytes32 *winner_account_id, + const uint8_t (*solution)[16], + uintptr_t winner_proof_index, + const uint8_t (*winner_proof_siblings)[32], + uintptr_t winner_proof_siblings_len, + struct FfiTransferResult *out_result); + +/** + * Claim a pinata reward using a private transaction for a not-yet-initialized owned account. + * + * Sends a privacy-preserving claim transaction for a winner account that has not yet + * been committed on-chain (i.e. is being initialized as part of this claim). + * + * # Parameters + * - `handle`: Valid wallet handle + * - `pinata_account_id`: The pinata program account ID + * - `winner_account_id`: The recipient private account ID (must be owned by this wallet) + * - `solution`: The solution value as little-endian [u8; 16] + * - `out_result`: Output pointer for the transaction result + * + * # Returns + * - `Success` if the claim transaction 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` + * - `pinata_account_id` must be a valid pointer to a `FfiBytes32` struct + * - `winner_account_id` must be a valid pointer to a `FfiBytes32` struct + * - `solution` 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_claim_pinata_private_owned_not_initialized(struct WalletHandle *handle, + const struct FfiBytes32 *pinata_account_id, + const struct FfiBytes32 *winner_account_id, + const uint8_t (*solution)[16], + struct FfiTransferResult *out_result); + /** * Synchronize private accounts to a specific block. * diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/pinata_interactions.rs index e588f400..abcfcf6a 100644 --- a/wallet/src/pinata_interactions.rs +++ b/wallet/src/pinata_interactions.rs @@ -126,7 +126,7 @@ impl WalletCore { &produce_random_nonces(1), &[(winner_npk.clone(), shared_secret_winner.clone())], &[], - &[] + &[], &program.into(), ) .unwrap(); diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index 6036a603..68891ff5 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -1,6 +1,6 @@ use common::{error::ExecutionFailureKind, rpc_primitives::requests::SendTxResponse}; use nssa::AccountId; -use nssa_core::SharedSecretKey; +use nssa_core::{MembershipProof, SharedSecretKey}; use crate::{PrivacyPreservingAccount, WalletCore}; @@ -25,6 +25,22 @@ impl Pinata<'_> { Ok(self.0.sequencer_client.send_tx_public(tx).await?) } + /// Claim a pinata reward using a privacy-preserving transaction for an already-initialized + /// owned private account. + /// + /// The `winner_proof` parameter is accepted for API completeness; the wallet currently fetches + /// the membership proof automatically from the chain. + pub async fn claim_private_owned_account_already_initialized( + &self, + pinata_account_id: AccountId, + winner_account_id: AccountId, + solution: u128, + _winner_proof: MembershipProof, + ) -> Result<(SendTxResponse, SharedSecretKey), ExecutionFailureKind> { + self.claim_private_owned_account(pinata_account_id, winner_account_id, solution) + .await + } + pub async fn claim_private_owned_account( &self, pinata_account_id: AccountId,