Merge pull request #340 from logos-blockchain/dsq/pinata-ffi

feat(ffi): Add pinata to wallet-ffi
This commit is contained in:
Daniel Sanchez 2026-02-20 17:21:19 +01:00 committed by GitHub
commit bc84d5cf31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 433 additions and 2 deletions

View File

@ -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;

308
wallet-ffi/src/pinata.rs Normal file
View File

@ -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,
}
}

View File

@ -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.
*

View File

@ -126,7 +126,7 @@ impl WalletCore {
&produce_random_nonces(1),
&[(winner_npk.clone(), shared_secret_winner.clone())],
&[],
&[]
&[],
&program.into(),
)
.unwrap();

View File

@ -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,