Merge 18e1bea512ab0c74e7ee701871628c93168b80ce into d3390efc6db215cef35ba1d6d1f5e13277fe9597

This commit is contained in:
Pravdyvy 2026-06-01 14:30:32 +03:00 committed by GitHub
commit 652f5434cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1394 additions and 9 deletions

View File

@ -16,4 +16,4 @@ runs:
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
run: |
curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/main/scripts/setup-logos-blockchain-circuits.sh | bash
curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/dd055cc1ef7c130f710a52a190edd97bc7b0f71b/scripts/setup-logos-blockchain-circuits.sh | bash

1
Cargo.lock generated
View File

@ -10720,6 +10720,7 @@ dependencies = [
"key_protocol",
"nssa",
"nssa_core",
"risc0-zkvm",
"sequencer_service_rpc",
"serde_json",
"tempfile",

View File

@ -7,6 +7,7 @@
clippy::undocumented_unsafe_blocks,
clippy::multiple_unsafe_ops_per_block,
clippy::shadow_unrelated,
clippy::as_conversions,
reason = "We don't care about these in tests"
)]
@ -21,13 +22,17 @@ use std::{
use anyhow::Result;
use integration_tests::{BlockingTestContext, TIME_TO_WAIT_FOR_BLOCK_SECONDS};
use log::info;
use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program};
use nssa::{
Account, AccountId, PrivateKey, PublicKey,
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
};
use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir;
use wallet::account::HumanReadableAccount;
use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, FfiU128, WalletHandle, error,
FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys,
FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error,
generic_transaction::{FfiProgramWithDependencies, FfiTransactionResult},
};
unsafe extern "C" {
@ -179,6 +184,42 @@ unsafe extern "C" {
handle: *mut WalletHandle,
out_block_height: *mut u64,
) -> error::WalletFfiError;
fn wallet_ffi_resolve_public_account(
account_id: FfiBytes32,
needs_sign: bool,
out_account_identity: *mut FfiAccountIdentity,
) -> error::WalletFfiError;
fn wallet_ffi_send_generic_public_transaction(
handle: *mut WalletHandle,
account_identities: *const FfiAccountIdentity,
account_identities_size: usize,
instruction_words: *const u32,
instruction_words_size: usize,
program_with_dependencies: *const FfiProgramWithDependencies,
out_result: *mut FfiTransactionResult,
) -> error::WalletFfiError;
fn wallet_ffi_resolve_private_account(
handle: *mut WalletHandle,
account_id: FfiBytes32,
out_account_identity: *mut FfiAccountIdentity,
) -> error::WalletFfiError;
fn wallet_ffi_send_generic_private_transaction(
handle: *mut WalletHandle,
account_identities: *const FfiAccountIdentity,
account_identities_size: usize,
instruction_words: *const u32,
instruction_words_size: usize,
program_with_dependencies: *const FfiProgramWithDependencies,
out_result: *mut FfiTransactionResult,
) -> error::WalletFfiError;
fn wallet_ffi_free_transaction_result(result: *mut FfiTransactionResult);
fn wallet_ffi_free_account_identity(account_identity: *mut FfiAccountIdentity);
}
fn new_wallet_ffi_with_test_context_config(
@ -1066,3 +1107,201 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
Ok(())
}
#[test]
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into();
let amount = 100_u128;
let mut transaction_result = FfiTransactionResult::default();
let mut from_account_identity = FfiAccountIdentity::default();
let mut to_account_identity = FfiAccountIdentity::default();
unsafe {
wallet_ffi_resolve_public_account(from, true, &raw mut from_account_identity).unwrap();
}
unsafe {
wallet_ffi_resolve_public_account(to, true, &raw mut to_account_identity).unwrap();
}
let ffi_accs = vec![from_account_identity, to_account_identity];
let account_identities_size = ffi_accs.len();
let account_identities =
Box::into_raw(ffi_accs.into_boxed_slice()) as *const FfiAccountIdentity;
let instruction_data =
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount,
})
.unwrap();
let instruction_words_size = instruction_data.len();
let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32;
let program: ProgramWithDependencies = Program::authenticated_transfer_program().into();
let program_with_dependencies: FfiProgramWithDependencies = program.into();
unsafe {
wallet_ffi_send_generic_public_transaction(
wallet_ffi_handle,
account_identities,
account_identities_size,
instruction_words,
instruction_words_size,
&raw const program_with_dependencies,
&raw mut transaction_result,
)
.unwrap();
}
info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let from_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const from,
true,
&raw mut out_balance,
)
.unwrap();
u128::from_le_bytes(out_balance)
};
let to_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
wallet_ffi_get_balance(wallet_ffi_handle, &raw const to, true, &raw mut out_balance)
.unwrap();
u128::from_le_bytes(out_balance)
};
assert_eq!(from_balance, 9900);
assert_eq!(to_balance, 20100);
unsafe {
let account_identities_mut = account_identities.cast_mut();
wallet_ffi_free_account_identity(account_identities_mut);
wallet_ffi_free_account_identity(account_identities_mut.add(1));
let instruction_data =
std::slice::from_raw_parts_mut(instruction_words.cast_mut(), instruction_words_size);
drop(Box::from_raw(std::ptr::from_mut(instruction_data)));
wallet_ffi_free_transaction_result(&raw mut transaction_result);
wallet_ffi_destroy(wallet_ffi_handle);
}
Ok(())
}
#[test]
fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let home = tempfile::tempdir()?;
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: FfiBytes32 = ctx.ctx().existing_private_accounts()[1].into();
let amount = 100_u128;
let mut transaction_result = FfiTransactionResult::default();
let mut from_account_identity = FfiAccountIdentity::default();
let mut to_account_identity = FfiAccountIdentity::default();
unsafe {
wallet_ffi_resolve_private_account(wallet_ffi_handle, from, &raw mut from_account_identity)
.unwrap();
}
unsafe {
wallet_ffi_resolve_private_account(wallet_ffi_handle, to, &raw mut to_account_identity)
.unwrap();
}
let ffi_accs = vec![from_account_identity, to_account_identity];
let account_identities_size = ffi_accs.len();
let account_identities =
Box::into_raw(ffi_accs.into_boxed_slice()) as *const FfiAccountIdentity;
let instruction_data =
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount,
})
.unwrap();
let instruction_words_size = instruction_data.len();
let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32;
let program: ProgramWithDependencies = Program::authenticated_transfer_program().into();
let program_with_dependencies: FfiProgramWithDependencies = program.into();
unsafe {
wallet_ffi_send_generic_private_transaction(
wallet_ffi_handle,
account_identities,
account_identities_size,
instruction_words,
instruction_words_size,
&raw const program_with_dependencies,
&raw mut transaction_result,
)
.unwrap();
}
assert_eq!(transaction_result.secrets_size, 2);
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, &raw mut current_height).unwrap();
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
let from_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
let _result = wallet_ffi_get_balance(
wallet_ffi_handle,
&raw const from,
false,
&raw mut out_balance,
);
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,
&raw const to,
false,
&raw mut out_balance,
);
u128::from_le_bytes(out_balance)
};
assert_eq!(from_balance, 9900);
assert_eq!(to_balance, 20100);
unsafe {
let account_identities_mut = account_identities.cast_mut();
wallet_ffi_free_account_identity(account_identities_mut);
wallet_ffi_free_account_identity(account_identities_mut.add(1));
let instruction_data =
std::slice::from_raw_parts_mut(instruction_words.cast_mut(), instruction_words_size);
drop(Box::from_raw(std::ptr::from_mut(instruction_data)));
wallet_ffi_free_transaction_result(&raw mut transaction_result);
wallet_ffi_destroy(wallet_ffi_handle);
}
Ok(())
}

View File

@ -22,8 +22,8 @@ const MAX_NUM_CYCLES_PUBLIC_EXECUTION: u64 = 1024 * 1024 * 32; // 32M cycles
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Program {
id: ProgramId,
elf: Vec<u8>,
pub id: ProgramId,
pub elf: Vec<u8>,
}
impl Program {

View File

@ -19,6 +19,7 @@ sequencer_service_rpc = { workspace = true, features = ["client"] }
tokio.workspace = true
key_protocol.workspace = true
serde_json.workspace = true
risc0-zkvm.workspace = true
[build-dependencies]
cbindgen = "0.29"

View File

@ -41,6 +41,8 @@ pub enum WalletFfiError {
InvalidTypeConversion = 15,
/// Invalid Key value.
InvalidKeyValue = 16,
/// Invalid program bytecode.
InvalidBytecode = 17,
/// Internal error (catch-all).
InternalError = 99,
}

View File

@ -0,0 +1,420 @@
use std::{
collections::HashMap,
ffi::{c_char, CString},
};
use nssa::{privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program};
use crate::{
block_on,
error::{print_error, WalletFfiError},
map_execution_error,
wallet::get_wallet,
FfiAccountIdentity, FfiBytes32, WalletHandle,
};
#[repr(C)]
pub struct SerializationHelperResult {
pub instruction_words: *mut u32,
pub instruction_words_size: usize,
pub error: WalletFfiError,
}
impl SerializationHelperResult {
const fn from_err(error: WalletFfiError) -> Self {
Self {
instruction_words: std::ptr::null_mut(),
instruction_words_size: 0,
error,
}
}
}
#[repr(C)]
/// Intended to be created manually.
pub struct FfiProgram {
pub elf_data: *const u8,
pub elf_size: usize,
}
impl TryFrom<&FfiProgram> for Program {
type Error = WalletFfiError;
fn try_from(value: &FfiProgram) -> Result<Self, Self::Error> {
let mut elf = Vec::with_capacity(value.elf_size);
// Alignment will be different, we need to read elements one-by-one
for i in 0..value.elf_size {
elf.push(unsafe { *value.elf_data.add(i) });
}
Self::new(elf).map_err(|err| {
print_error(format!("Invalid program bytecode, err: {err}"));
WalletFfiError::InvalidBytecode
})
}
}
impl From<Program> for FfiProgram {
fn from(value: Program) -> Self {
let elf_size = value.elf.len();
let elf_data = Box::into_raw(value.elf.into_boxed_slice()) as *const u8;
Self { elf_data, elf_size }
}
}
#[repr(C)]
/// Intended to be created manually.
pub struct FfiProgramWithDependencies {
pub program: FfiProgram,
pub deps: *const FfiProgram,
pub deps_size: usize,
}
impl TryFrom<&FfiProgramWithDependencies> for ProgramWithDependencies {
type Error = WalletFfiError;
fn try_from(value: &FfiProgramWithDependencies) -> Result<Self, Self::Error> {
let mut program_map = HashMap::new();
let orig_program = (&value.program).try_into()?;
// Alignment will be different, we need to read elements one-by-one
for i in 0..value.deps_size {
let program_dep: Program = unsafe { value.deps.add(i).as_ref() }
.ok_or(WalletFfiError::NullPointer)?
.try_into()?;
program_map.insert(program_dep.id(), program_dep);
}
Ok(Self {
program: orig_program,
dependencies: program_map,
})
}
}
impl From<ProgramWithDependencies> for FfiProgramWithDependencies {
fn from(value: ProgramWithDependencies) -> Self {
let ffi_program = value.program.into();
let ffi_deps: Vec<FfiProgram> = value
.dependencies
.into_values()
.map(Into::into)
.collect::<Vec<_>>();
let deps_size = ffi_deps.len();
let deps = Box::into_raw(ffi_deps.into_boxed_slice()) as *const FfiProgram;
Self {
program: ffi_program,
deps,
deps_size,
}
}
}
/// Result of a generic transaction operation.
#[repr(C)]
pub struct FfiTransactionResult {
// TODO: Replace with HashType FFI representation
/// Transaction hash (null-terminated string, or null on failure).
pub tx_hash: *mut c_char,
/// Whether the transaction succeeded.
pub success: bool,
pub secrets_data: *const FfiBytes32,
/// Public transactions have 0 secrets.
pub secrets_size: usize,
}
impl Default for FfiTransactionResult {
fn default() -> Self {
Self {
tx_hash: std::ptr::null_mut(),
success: false,
secrets_data: std::ptr::null(),
secrets_size: 0,
}
}
}
/// Serialize sequence of bytes into RISC0 readable words.
///
/// # Parameters
/// - `input_instruction_data`: Valid pointer to a sequence of bytes
/// - `input_instruction_data_size`: Size of `input_instruction_data`
///
/// # Returns
/// - `Success` on successful creation
/// - Error code on failure
///
/// # Safety
/// - `input_instruction_data` must be a valid pointer
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_serialization_helper(
input_instruction_data: *const u8,
input_instruction_data_size: usize,
) -> SerializationHelperResult {
if input_instruction_data.is_null() {
print_error("Null input pointer for instruction_data");
return SerializationHelperResult::from_err(WalletFfiError::NullPointer);
}
let input_slice =
unsafe { std::slice::from_raw_parts(input_instruction_data, input_instruction_data_size) };
let res_vec_u32_with_prefix = match risc0_zkvm::serde::to_vec(input_slice).map_err(|err| {
print_error(format!(
"Failed to serialize input into words with err {err}"
));
WalletFfiError::SerializationError
}) {
Ok(res) => res,
Err(err) => return SerializationHelperResult::from_err(err),
};
// The resulting vec contains len as prefix
let res_vec_u32 = res_vec_u32_with_prefix[1..].to_vec();
let res_len = res_vec_u32.len();
let res_boxed = res_vec_u32.into_boxed_slice();
let res_ptr = Box::into_raw(res_boxed).cast::<u32>();
SerializationHelperResult {
instruction_words: res_ptr,
instruction_words_size: res_len,
error: WalletFfiError::Success,
}
}
/// Send generic public transaction.
///
/// # Parameters
/// - `handle`: Valid pointer to wallet handle
/// - `account_identities`: Valid pointer to list of `FfiAccountIdentity`
/// - `instruction_words`: Valid pointer to instruction words
/// - `out_result`: Valid pointer to `FfiTransactionResult`
///
/// # Returns
/// - `Success` on successful creation
/// - Error code on failure
///
/// # Safety
/// - `handle` must be a valid pointer
/// - `account_identities` must be a valid pointer
/// - `instruction_words` must be a valid pointer
/// - `out_result` must be a valid pointer
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_send_generic_public_transaction(
handle: *mut WalletHandle,
account_identities: *const FfiAccountIdentity,
account_identities_size: usize,
instruction_words: *const u32,
instruction_words_size: usize,
program_with_dependencies: *const FfiProgramWithDependencies,
out_result: *mut FfiTransactionResult,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
Ok(w) => w,
Err(e) => return e,
};
if account_identities.is_null() {
print_error("Null input pointer for account identities list");
return WalletFfiError::NullPointer;
}
if instruction_words.is_null() {
print_error("Null input pointer for instruction data");
return WalletFfiError::NullPointer;
}
if out_result.is_null() {
print_error("Null output pointer return hash");
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 accounts_ffi = std::slice::from_raw_parts(account_identities, account_identities_size);
let instruction_data = std::slice::from_raw_parts(instruction_words, instruction_words_size);
let mut accounts = Vec::with_capacity(account_identities_size);
for ffi_acc in accounts_ffi {
match ffi_acc.try_into() {
Ok(v) => accounts.push(v),
Err(err) => {
print_error("Failed to convert FfiAccountIdentity into AccountIdentity");
return err;
}
}
}
let program = match unsafe { &*program_with_dependencies }.try_into() {
Ok(v) => v,
Err(err) => return err,
};
match block_on(wallet.send_pub_tx(accounts, instruction_data.to_vec(), &program)) {
Ok(tx_hash) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(std::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!("Public send failed: {e:?}"));
unsafe {
(*out_result).tx_hash = std::ptr::null_mut();
(*out_result).success = false;
}
map_execution_error(e)
}
}
}
/// Send generic private transaction.
///
/// # Parameters
/// - `handle`: Valid pointer to wallet handle
/// - `account_identities`: Valid pointer to list of `FfiAccountIdentity`
/// - `instruction_words`: Valid pointer to instruction words
/// - `out_result`: Valid pointer to `FfiTransactionResult`
///
/// # Returns
/// - `Success` on successful creation
/// - Error code on failure
///
/// # Safety
/// - `handle` must be a valid pointer
/// - `account_identities` must be a valid pointer
/// - `instruction_words` must be a valid pointer
/// - `out_result` must be a valid pointer
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_send_generic_private_transaction(
handle: *mut WalletHandle,
account_identities: *const FfiAccountIdentity,
account_identities_size: usize,
instruction_words: *const u32,
instruction_words_size: usize,
program_with_dependencies: *const FfiProgramWithDependencies,
out_result: *mut FfiTransactionResult,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
Ok(w) => w,
Err(e) => return e,
};
if account_identities.is_null() {
print_error("Null input pointer for account identities list");
return WalletFfiError::NullPointer;
}
if instruction_words.is_null() {
print_error("Null input pointer for instruction data");
return WalletFfiError::NullPointer;
}
if out_result.is_null() {
print_error("Null output pointer return hash");
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 accounts_ffi = std::slice::from_raw_parts(account_identities, account_identities_size);
let instruction_data = std::slice::from_raw_parts(instruction_words, instruction_words_size);
let mut accounts = Vec::with_capacity(account_identities_size);
for ffi_acc in accounts_ffi {
match ffi_acc.try_into() {
Ok(v) => accounts.push(v),
Err(err) => {
print_error("Failed to convert FfiAccountIdentity into AccountIdentity");
return err;
}
}
}
let program = match unsafe { &*program_with_dependencies }.try_into() {
Ok(v) => v,
Err(err) => return err,
};
match block_on(wallet.send_privacy_preserving_tx(accounts, instruction_data.to_vec(), &program))
{
Ok((tx_hash, secrets)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(std::ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
(*out_result).success = true;
let secrets_size = secrets.len();
let boxed_slice = secrets
.into_iter()
.map(Into::into)
.collect::<Vec<FfiBytes32>>()
.into_boxed_slice();
let secrets_data = Box::into_raw(boxed_slice) as *const FfiBytes32;
(*out_result).secrets_size = secrets_size;
(*out_result).secrets_data = secrets_data;
}
WalletFfiError::Success
}
Err(e) => {
print_error(format!("Private send failed: {e:?}"));
unsafe {
*out_result = FfiTransactionResult::default();
}
map_execution_error(e)
}
}
}
/// Free a transaction result returned by `wallet_ffi_send_generic_public_transaction` or
/// `wallet_ffi_send_generic_private_transaction`.
///
/// # Safety
/// The result must be either null or a valid result from a transaction function.
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_free_transaction_result(result: *mut FfiTransactionResult) {
if result.is_null() {
return;
}
unsafe {
let result = &*result;
if !result.tx_hash.is_null() {
drop(CString::from_raw(result.tx_hash));
}
if !result.secrets_data.is_null() {
let secrets =
std::slice::from_raw_parts_mut(result.secrets_data.cast_mut(), result.secrets_size);
drop(Box::from_raw(std::ptr::from_mut::<[FfiBytes32]>(secrets)));
}
}
}

View File

@ -3,11 +3,13 @@
use std::ptr;
use nssa::{AccountId, PublicKey};
use wallet::AccountIdentity;
use crate::{
error::{print_error, WalletFfiError},
types::{FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, WalletHandle},
wallet::get_wallet,
FfiAccountIdentity,
};
/// Get the public key for a public account.
@ -250,3 +252,127 @@ pub unsafe extern "C" fn wallet_ffi_account_id_from_base58(
WalletFfiError::Success
}
/// Resolve public account.
///
/// # Parameters
/// - `account_id`: 32 bytes of the public account ID
/// - `needs_sign`: whether the account needs signing
/// - `out_account_identity`: valid pointer, where output will be written
///
/// # Returns
/// - `Success` on successful retrieval
///
/// # Safety
/// - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_resolve_public_account(
account_id: FfiBytes32,
needs_sign: bool,
out_account_identity: *mut FfiAccountIdentity,
) -> WalletFfiError {
if out_account_identity.is_null() {
print_error("Null pointer argument");
return WalletFfiError::NullPointer;
}
let resolved_account = if needs_sign {
AccountIdentity::Public(account_id.into())
} else {
AccountIdentity::PublicNoSign(account_id.into())
};
unsafe {
*out_account_identity = resolved_account.into();
}
WalletFfiError::Success
}
/// Resolve private account.
///
/// # Parameters
/// - `handle`: Valid wallet handle
/// - `account_id`: 32 bytes of the public account ID
/// - `out_account_identity`: valid pointer, where output will be written
///
/// # Returns
/// - `Success` on successful retrieval
/// - `InternalError` if failed to lock wallet
/// - `AccountNotFound` if the account is not found
///
/// # Safety
/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
/// - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_resolve_private_account(
handle: *mut WalletHandle,
account_id: FfiBytes32,
out_account_identity: *mut FfiAccountIdentity,
) -> WalletFfiError {
if out_account_identity.is_null() {
print_error("Null pointer argument");
return WalletFfiError::NullPointer;
}
let wrapper = match get_wallet(handle) {
Ok(w) => w,
Err(e) => return e,
};
let wallet = match wrapper.core.lock() {
Ok(w) => w,
Err(e) => {
print_error(format!("Failed to lock wallet: {e}"));
return WalletFfiError::InternalError;
}
};
let account_id = account_id.into();
let Some(resolved_account) = wallet.resolve_private_account(account_id) else {
print_error("Account not found");
return WalletFfiError::AccountNotFound;
};
unsafe {
*out_account_identity = resolved_account.into();
}
WalletFfiError::Success
}
/// Free account identity returned by `wallet_ffi_resolve_private_account` or
/// `wallet_ffi_resolve_public_account`.
///
/// # Safety
/// The account must be either null or a valid account returned by
/// `wallet_ffi_resolve_private_account` or `wallet_ffi_resolve_public_account`.
#[no_mangle]
pub unsafe extern "C" fn wallet_ffi_free_account_identity(
account_identity: *mut FfiAccountIdentity,
) {
if account_identity.is_null() {
return;
}
unsafe {
let FfiAccountIdentity {
kind: _,
account_id: _,
nullifier_secret_key: _,
nullifier_public_key: _,
viewing_public_key,
viewing_public_key_len,
identifier: _,
} = *account_identity;
if !viewing_public_key.is_null() {
let slice = std::slice::from_raw_parts_mut(
viewing_public_key.cast_mut(),
viewing_public_key_len,
);
drop(Box::from_raw(std::ptr::from_mut::<[u8]>(slice)));
}
}
}

View File

@ -23,6 +23,7 @@
#![expect(
clippy::undocumented_unsafe_blocks,
clippy::multiple_unsafe_ops_per_block,
clippy::as_conversions,
reason = "TODO: fix later"
)]
@ -42,6 +43,7 @@ use crate::error::print_error;
pub mod account;
pub mod error;
pub mod generic_transaction;
pub mod keys;
pub mod pinata;
pub mod sync;

View File

@ -3,8 +3,9 @@
use core::slice;
use std::{ffi::c_char, ptr};
use nssa::Data;
use nssa_core::encryption::shared_key_derivation::Secp256k1Point;
use nssa::{Data, SharedSecretKey};
use nssa_core::{encryption::shared_key_derivation::Secp256k1Point, NullifierPublicKey};
use wallet::AccountIdentity;
use crate::error::WalletFfiError;
@ -154,6 +155,12 @@ impl FfiBytes32 {
}
}
impl From<SharedSecretKey> for FfiBytes32 {
fn from(value: SharedSecretKey) -> Self {
Self { data: value.0 }
}
}
impl FfiPrivateAccountKeys {
#[must_use]
pub const fn npk(&self) -> nssa_core::NullifierPublicKey {
@ -172,6 +179,46 @@ impl FfiPrivateAccountKeys {
}
}
/// Enumeration to represent kinds of `FfiAccountIdentity`.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FfiAccountIdentityKind {
Public = 0,
PublicNoSign = 1,
PrivateOwned = 2,
PrivateForeign = 3,
PrivatePdaOwned = 4,
PrivatePdaForeign = 5,
PrivateShared = 6,
PrivatePdaShared = 7,
}
/// Struct representing an account identity, given to `AccountManager` at intialization.
#[repr(C)]
pub struct FfiAccountIdentity {
pub kind: FfiAccountIdentityKind,
pub account_id: FfiBytes32,
pub nullifier_secret_key: FfiBytes32,
pub nullifier_public_key: FfiBytes32,
pub viewing_public_key: *const u8,
pub viewing_public_key_len: usize,
pub identifier: FfiU128,
}
impl Default for FfiAccountIdentity {
fn default() -> Self {
Self {
kind: FfiAccountIdentityKind::Public,
account_id: FfiBytes32::default(),
nullifier_secret_key: FfiBytes32::default(),
nullifier_public_key: FfiBytes32::default(),
viewing_public_key: std::ptr::null(),
viewing_public_key_len: 0,
identifier: FfiU128::default(),
}
}
}
impl From<u128> for FfiU128 {
fn from(value: u128) -> Self {
Self {
@ -192,6 +239,12 @@ impl From<nssa::AccountId> for FfiBytes32 {
}
}
impl From<[u8; 32]> for FfiBytes32 {
fn from(value: [u8; 32]) -> Self {
Self { data: value }
}
}
impl From<FfiBytes32> for nssa::AccountId {
fn from(bytes: FfiBytes32) -> Self {
Self::new(bytes.data)
@ -266,3 +319,342 @@ impl TryFrom<&FfiPublicAccountKey> for nssa::PublicKey {
Ok(public_key)
}
}
impl From<AccountIdentity> for FfiAccountIdentity {
fn from(value: AccountIdentity) -> Self {
match value {
AccountIdentity::Public(account_id) => Self {
kind: FfiAccountIdentityKind::Public,
account_id: account_id.into(),
..Default::default()
},
AccountIdentity::PublicNoSign(account_id) => Self {
kind: FfiAccountIdentityKind::PublicNoSign,
account_id: account_id.into(),
..Default::default()
},
AccountIdentity::PrivateOwned(account_id) => Self {
kind: FfiAccountIdentityKind::PrivateOwned,
account_id: account_id.into(),
..Default::default()
},
AccountIdentity::PrivateForeign {
npk,
vpk,
identifier,
} => {
let vpk_vec = vpk.0;
let vpk_len = vpk_vec.len();
let vpk_data = if vpk_len > 0 {
let vpk_data_boxed = vpk_vec.into_boxed_slice();
Box::into_raw(vpk_data_boxed) as *const u8
} else {
ptr::null()
};
Self {
kind: FfiAccountIdentityKind::PrivateForeign,
nullifier_public_key: npk.0.into(),
viewing_public_key: vpk_data,
viewing_public_key_len: vpk_len,
identifier: identifier.into(),
..Default::default()
}
}
AccountIdentity::PrivatePdaOwned(account_id) => Self {
kind: FfiAccountIdentityKind::PrivatePdaOwned,
account_id: account_id.into(),
..Default::default()
},
AccountIdentity::PrivatePdaForeign {
account_id,
npk,
vpk,
identifier,
} => {
let vpk_vec = vpk.0;
let vpk_len = vpk_vec.len();
let vpk_data = if vpk_len > 0 {
let vpk_data_boxed = vpk_vec.into_boxed_slice();
Box::into_raw(vpk_data_boxed) as *const u8
} else {
ptr::null()
};
Self {
kind: FfiAccountIdentityKind::PrivatePdaForeign,
account_id: account_id.into(),
nullifier_public_key: npk.0.into(),
viewing_public_key: vpk_data,
viewing_public_key_len: vpk_len,
identifier: identifier.into(),
..Default::default()
}
}
AccountIdentity::PrivateShared {
nsk,
npk,
vpk,
identifier,
} => {
let vpk_vec = vpk.0;
let vpk_len = vpk_vec.len();
let vpk_data = if vpk_len > 0 {
let vpk_data_boxed = vpk_vec.into_boxed_slice();
Box::into_raw(vpk_data_boxed) as *const u8
} else {
ptr::null()
};
Self {
kind: FfiAccountIdentityKind::PrivateShared,
nullifier_secret_key: nsk.into(),
nullifier_public_key: npk.0.into(),
viewing_public_key: vpk_data,
viewing_public_key_len: vpk_len,
identifier: identifier.into(),
..Default::default()
}
}
AccountIdentity::PrivatePdaShared {
account_id,
nsk,
npk,
vpk,
identifier,
} => {
let vpk_vec = vpk.0;
let vpk_len = vpk_vec.len();
let vpk_data = if vpk_len > 0 {
let vpk_data_boxed = vpk_vec.into_boxed_slice();
Box::into_raw(vpk_data_boxed) as *const u8
} else {
ptr::null()
};
Self {
kind: FfiAccountIdentityKind::PrivatePdaShared,
account_id: account_id.into(),
nullifier_secret_key: nsk.into(),
nullifier_public_key: npk.0.into(),
viewing_public_key: vpk_data,
viewing_public_key_len: vpk_len,
identifier: identifier.into(),
}
}
}
}
}
impl TryFrom<&FfiAccountIdentity> for AccountIdentity {
type Error = WalletFfiError;
fn try_from(value: &FfiAccountIdentity) -> Result<Self, Self::Error> {
match value.kind {
FfiAccountIdentityKind::Public => Ok(Self::Public(value.account_id.into())),
FfiAccountIdentityKind::PublicNoSign => Ok(Self::PublicNoSign(value.account_id.into())),
FfiAccountIdentityKind::PrivateOwned => Ok(Self::PrivateOwned(value.account_id.into())),
FfiAccountIdentityKind::PrivateForeign => {
let vpk = if value.viewing_public_key_len == 33 {
let slice = unsafe {
slice::from_raw_parts(
value.viewing_public_key,
value.viewing_public_key_len,
)
};
Ok(Secp256k1Point(slice.to_vec()))
} else {
Err(WalletFfiError::InvalidKeyValue)
}?;
Ok(Self::PrivateForeign {
npk: NullifierPublicKey(value.nullifier_public_key.data),
vpk,
identifier: value.identifier.into(),
})
}
FfiAccountIdentityKind::PrivatePdaOwned => {
Ok(Self::PrivatePdaOwned(value.account_id.into()))
}
FfiAccountIdentityKind::PrivatePdaForeign => {
let vpk = if value.viewing_public_key_len == 33 {
let slice = unsafe {
slice::from_raw_parts(
value.viewing_public_key,
value.viewing_public_key_len,
)
};
Ok(Secp256k1Point(slice.to_vec()))
} else {
Err(WalletFfiError::InvalidKeyValue)
}?;
Ok(Self::PrivatePdaForeign {
account_id: value.account_id.into(),
npk: NullifierPublicKey(value.nullifier_public_key.data),
vpk,
identifier: value.identifier.into(),
})
}
FfiAccountIdentityKind::PrivateShared => {
let vpk = if value.viewing_public_key_len == 33 {
let slice = unsafe {
slice::from_raw_parts(
value.viewing_public_key,
value.viewing_public_key_len,
)
};
Ok(Secp256k1Point(slice.to_vec()))
} else {
Err(WalletFfiError::InvalidKeyValue)
}?;
Ok(Self::PrivateShared {
nsk: value.nullifier_secret_key.data,
npk: NullifierPublicKey(value.nullifier_public_key.data),
vpk,
identifier: value.identifier.into(),
})
}
FfiAccountIdentityKind::PrivatePdaShared => {
let vpk = if value.viewing_public_key_len == 33 {
let slice = unsafe {
slice::from_raw_parts(
value.viewing_public_key,
value.viewing_public_key_len,
)
};
Ok(Secp256k1Point(slice.to_vec()))
} else {
Err(WalletFfiError::InvalidKeyValue)
}?;
Ok(Self::PrivatePdaShared {
account_id: value.account_id.into(),
nsk: value.nullifier_secret_key.data,
npk: NullifierPublicKey(value.nullifier_public_key.data),
vpk,
identifier: value.identifier.into(),
})
}
}
}
}
#[cfg(test)]
mod tests {
use nssa::{AccountId, PrivateKey, PublicKey};
use nssa_core::{encryption::ViewingPublicKey, program::PdaSeed, PrivateAccountKind};
use wallet::AccountIdentity;
use crate::{FfiAccountIdentity, FfiAccountIdentityKind};
#[test]
fn account_identity_roundtrip() {
let private_key = PrivateKey::try_new([42; 32]).unwrap();
let public_key = PublicKey::new_from_private_key(&private_key);
let pub_acc_id = (&public_key).into();
let nsk = [43; 32];
let vpk = ViewingPublicKey::from_scalar([44; 32]);
let npk = (&nsk).into();
let identifier = u128::from_le_bytes([45; 16]);
let private_reg_acc_id =
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier));
let private_pda_acc_id = AccountId::for_private_account(
&npk,
&PrivateAccountKind::Pda {
program_id: [46; 8],
seed: PdaSeed::new([47; 32]),
identifier,
},
);
let acc_identity_1 = AccountIdentity::Public(pub_acc_id);
let acc_identity_2 = AccountIdentity::PublicNoSign(pub_acc_id);
let acc_identity_3 = AccountIdentity::PrivateOwned(private_reg_acc_id);
let acc_identity_4 = AccountIdentity::PrivateForeign {
npk,
vpk: vpk.clone(),
identifier,
};
let acc_identity_5 = AccountIdentity::PrivatePdaOwned(private_pda_acc_id);
let acc_identity_6 = AccountIdentity::PrivatePdaForeign {
account_id: private_pda_acc_id,
npk,
vpk: vpk.clone(),
identifier,
};
let acc_identity_7 = AccountIdentity::PrivateShared {
nsk,
npk,
vpk: vpk.clone(),
identifier,
};
let acc_identity_8 = AccountIdentity::PrivatePdaShared {
account_id: private_pda_acc_id,
nsk,
npk,
vpk,
identifier,
};
let ffi_acc_identity_1: FfiAccountIdentity = acc_identity_1.clone().into();
let ffi_acc_identity_2: FfiAccountIdentity = acc_identity_2.clone().into();
let ffi_acc_identity_3: FfiAccountIdentity = acc_identity_3.clone().into();
let ffi_acc_identity_4: FfiAccountIdentity = acc_identity_4.clone().into();
let ffi_acc_identity_5: FfiAccountIdentity = acc_identity_5.clone().into();
let ffi_acc_identity_6: FfiAccountIdentity = acc_identity_6.clone().into();
let ffi_acc_identity_7: FfiAccountIdentity = acc_identity_7.clone().into();
let ffi_acc_identity_8: FfiAccountIdentity = acc_identity_8.clone().into();
assert_eq!(ffi_acc_identity_1.kind, FfiAccountIdentityKind::Public);
assert_eq!(
ffi_acc_identity_2.kind,
FfiAccountIdentityKind::PublicNoSign
);
assert_eq!(
ffi_acc_identity_3.kind,
FfiAccountIdentityKind::PrivateOwned
);
assert_eq!(
ffi_acc_identity_4.kind,
FfiAccountIdentityKind::PrivateForeign
);
assert_eq!(
ffi_acc_identity_5.kind,
FfiAccountIdentityKind::PrivatePdaOwned
);
assert_eq!(
ffi_acc_identity_6.kind,
FfiAccountIdentityKind::PrivatePdaForeign
);
assert_eq!(
ffi_acc_identity_7.kind,
FfiAccountIdentityKind::PrivateShared
);
assert_eq!(
ffi_acc_identity_8.kind,
FfiAccountIdentityKind::PrivatePdaShared
);
let acc_identity_res_1: AccountIdentity = (&ffi_acc_identity_1).try_into().unwrap();
let acc_identity_res_2: AccountIdentity = (&ffi_acc_identity_2).try_into().unwrap();
let acc_identity_res_3: AccountIdentity = (&ffi_acc_identity_3).try_into().unwrap();
let acc_identity_res_4: AccountIdentity = (&ffi_acc_identity_4).try_into().unwrap();
let acc_identity_res_5: AccountIdentity = (&ffi_acc_identity_5).try_into().unwrap();
let acc_identity_res_6: AccountIdentity = (&ffi_acc_identity_6).try_into().unwrap();
let acc_identity_res_7: AccountIdentity = (&ffi_acc_identity_7).try_into().unwrap();
let acc_identity_res_8: AccountIdentity = (&ffi_acc_identity_8).try_into().unwrap();
assert_eq!(acc_identity_res_1, acc_identity_1);
assert_eq!(acc_identity_res_2, acc_identity_2);
assert_eq!(acc_identity_res_3, acc_identity_3);
assert_eq!(acc_identity_res_4, acc_identity_4);
assert_eq!(acc_identity_res_5, acc_identity_5);
assert_eq!(acc_identity_res_6, acc_identity_6);
assert_eq!(acc_identity_res_7, acc_identity_7);
assert_eq!(acc_identity_res_8, acc_identity_8);
}
}

View File

@ -103,12 +103,30 @@ typedef enum WalletFfiError {
* Invalid Key value.
*/
INVALID_KEY_VALUE = 16,
/**
* Invalid program bytecode.
*/
INVALID_BYTECODE = 17,
/**
* Internal error (catch-all).
*/
INTERNAL_ERROR = 99,
} WalletFfiError;
/**
* Enumeration to represent kinds of `FfiAccountIdentity`.
*/
typedef enum FfiAccountIdentityKind {
PUBLIC = 0,
PUBLIC_NO_SIGN = 1,
PRIVATE_OWNED = 2,
PRIVATE_FOREIGN = 3,
PRIVATE_PDA_OWNED = 4,
PRIVATE_PDA_FOREIGN = 5,
PRIVATE_SHARED = 6,
PRIVATE_PDA_SHARED = 7,
} FfiAccountIdentityKind;
/**
* Opaque pointer to the Wallet instance.
*
@ -200,6 +218,61 @@ typedef struct FfiAccount {
struct FfiU128 nonce;
} FfiAccount;
typedef struct SerializationHelperResult {
uint32_t *instruction_words;
uintptr_t instruction_words_size;
enum WalletFfiError error;
} SerializationHelperResult;
/**
* Struct representing an account identity, given to `AccountManager` at intialization.
*/
typedef struct FfiAccountIdentity {
enum FfiAccountIdentityKind kind;
struct FfiBytes32 account_id;
struct FfiBytes32 nullifier_secret_key;
struct FfiBytes32 nullifier_public_key;
const uint8_t *viewing_public_key;
uintptr_t viewing_public_key_len;
struct FfiU128 identifier;
} FfiAccountIdentity;
/**
* Intended to be created manually.
*/
typedef struct FfiProgram {
const uint8_t *elf_data;
uintptr_t elf_size;
} FfiProgram;
/**
* Intended to be created manually.
*/
typedef struct FfiProgramWithDependencies {
struct FfiProgram program;
const struct FfiProgram *deps;
uintptr_t deps_size;
} FfiProgramWithDependencies;
/**
* Result of a generic transaction operation.
*/
typedef struct FfiTransactionResult {
/**
* Transaction hash (null-terminated string, or null on failure).
*/
char *tx_hash;
/**
* Whether the transaction succeeded.
*/
bool success;
const struct FfiBytes32 *secrets_data;
/**
* Public transactions have 0 secrets.
*/
uintptr_t secrets_size;
} FfiTransactionResult;
/**
* Public key info for a public account.
*/
@ -454,6 +527,86 @@ enum WalletFfiError wallet_ffi_import_private_account(struct WalletHandle *handl
const struct FfiU128 *identifier,
const char *account_state_json);
/**
* Serialize sequence of bytes into RISC0 readable words.
*
* # Parameters
* - `input_instruction_data`: Valid pointer to a sequence of bytes
* - `input_instruction_data_size`: Size of `input_instruction_data`
*
* # Returns
* - `Success` on successful creation
* - Error code on failure
*
* # Safety
* - `input_instruction_data` must be a valid pointer
*/
struct SerializationHelperResult wallet_ffi_serialization_helper(const uint8_t *input_instruction_data,
uintptr_t input_instruction_data_size);
/**
* Send generic public transaction.
*
* # Parameters
* - `handle`: Valid pointer to wallet handle
* - `account_identities`: Valid pointer to list of `FfiAccountIdentity`
* - `instruction_words`: Valid pointer to instruction words
* - `out_result`: Valid pointer to `FfiTransactionResult`
*
* # Returns
* - `Success` on successful creation
* - Error code on failure
*
* # Safety
* - `handle` must be a valid pointer
* - `account_identities` must be a valid pointer
* - `instruction_words` must be a valid pointer
* - `out_result` must be a valid pointer
*/
enum WalletFfiError wallet_ffi_send_generic_public_transaction(struct WalletHandle *handle,
const struct FfiAccountIdentity *account_identities,
uintptr_t account_identities_size,
const uint32_t *instruction_words,
uintptr_t instruction_words_size,
const struct FfiProgramWithDependencies *program_with_dependencies,
struct FfiTransactionResult *out_result);
/**
* Send generic private transaction.
*
* # Parameters
* - `handle`: Valid pointer to wallet handle
* - `account_identities`: Valid pointer to list of `FfiAccountIdentity`
* - `instruction_words`: Valid pointer to instruction words
* - `out_result`: Valid pointer to `FfiTransactionResult`
*
* # Returns
* - `Success` on successful creation
* - Error code on failure
*
* # Safety
* - `handle` must be a valid pointer
* - `account_identities` must be a valid pointer
* - `instruction_words` must be a valid pointer
* - `out_result` must be a valid pointer
*/
enum WalletFfiError wallet_ffi_send_generic_private_transaction(struct WalletHandle *handle,
const struct FfiAccountIdentity *account_identities,
uintptr_t account_identities_size,
const uint32_t *instruction_words,
uintptr_t instruction_words_size,
const struct FfiProgramWithDependencies *program_with_dependencies,
struct FfiTransactionResult *out_result);
/**
* Free a transaction result returned by `wallet_ffi_send_generic_public_transaction` or
* `wallet_ffi_send_generic_private_transaction`.
*
* # Safety
* The result must be either null or a valid result from a transaction function.
*/
void wallet_ffi_free_transaction_result(struct FfiTransactionResult *result);
/**
* Get the public key for a public account.
*
@ -552,6 +705,55 @@ 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);
/**
* Resolve public account.
*
* # Parameters
* - `account_id`: 32 bytes of the public account ID
* - `needs_sign`: whether the account needs signing
* - `out_account_identity`: valid pointer, where output will be written
*
* # Returns
* - `Success` on successful retrieval
*
* # Safety
* - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct
*/
enum WalletFfiError wallet_ffi_resolve_public_account(struct FfiBytes32 account_id,
bool needs_sign,
struct FfiAccountIdentity *out_account_identity);
/**
* Resolve private account.
*
* # Parameters
* - `handle`: Valid wallet handle
* - `account_id`: 32 bytes of the public account ID
* - `out_account_identity`: valid pointer, where output will be written
*
* # Returns
* - `Success` on successful retrieval
* - `InternalError` if failed to lock wallet
* - `AccountNotFound` if the account is not found
*
* # Safety
* - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open`
* - `out_account_identity` must be a valid pointer to a `FfiAccountIdentity` struct
*/
enum WalletFfiError wallet_ffi_resolve_private_account(struct WalletHandle *handle,
struct FfiBytes32 account_id,
struct FfiAccountIdentity *out_account_identity);
/**
* Free account identity returned by `wallet_ffi_resolve_private_account` or
* `wallet_ffi_resolve_public_account`.
*
* # Safety
* The account must be either null or a valid account returned by
* `wallet_ffi_resolve_private_account` or `wallet_ffi_resolve_public_account`.
*/
void wallet_ffi_free_account_identity(struct FfiAccountIdentity *account_identity);
/**
* Claim a pinata reward using a public transaction.
*

View File

@ -10,7 +10,7 @@ use nssa_core::{
use crate::{ExecutionFailureKind, WalletCore};
#[derive(Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AccountIdentity {
Public(AccountId),
/// A public account without signing. Would not try to sign, even if account is owned.