//! C-compatible type definitions for the FFI layer. use core::slice; use std::{ffi::c_char, ptr}; use nssa::{Data, SharedSecretKey}; use nssa_core::{encryption::shared_key_derivation::Secp256k1Point, NullifierPublicKey}; use wallet::AccountIdentity; use crate::error::WalletFfiError; /// Opaque pointer to the Wallet instance. /// /// This type is never instantiated directly - it's used as an opaque handle /// to hide the internal wallet structure from C code. #[repr(C)] pub struct WalletHandle { _private: [u8; 0], } /// 32-byte array type for `AccountId`, keys, hashes, etc. #[repr(C)] #[derive(Clone, Copy, Default)] pub struct FfiBytes32 { pub data: [u8; 32], } /// Program ID - 8 u32 values (32 bytes total). #[repr(C)] #[derive(Clone, Copy, Default)] pub struct FfiProgramId { pub data: [u32; 8], } /// U128 - 16 bytes little endian. #[repr(C)] #[derive(Clone, Copy, Default)] pub struct FfiU128 { pub data: [u8; 16], } /// Account data structure - C-compatible version of nssa Account. /// /// Note: `balance` and `nonce` are u128 values represented as little-endian /// byte arrays since C doesn't have native u128 support. #[repr(C)] pub struct FfiAccount { pub program_owner: FfiProgramId, /// Balance as little-endian [u8; 16]. pub balance: FfiU128, /// Pointer to account data bytes. pub data: *const u8, /// Length of account data. pub data_len: usize, /// Nonce as little-endian [u8; 16]. pub nonce: FfiU128, } impl Default for FfiAccount { fn default() -> Self { Self { program_owner: FfiProgramId::default(), balance: FfiU128::default(), data: std::ptr::null(), data_len: 0, nonce: FfiU128::default(), } } } /// Public keys for a private account (safe to expose). #[repr(C)] pub struct FfiPrivateAccountKeys { /// Nullifier public key (32 bytes). pub nullifier_public_key: FfiBytes32, /// viewing public key (compressed secp256k1 point). pub viewing_public_key: *const u8, /// Length of viewing public key (typically 33 bytes). pub viewing_public_key_len: usize, } impl Default for FfiPrivateAccountKeys { fn default() -> Self { Self { nullifier_public_key: FfiBytes32::default(), viewing_public_key: std::ptr::null(), viewing_public_key_len: 0, } } } /// Public key info for a public account. #[repr(C)] #[derive(Clone, Copy, Default)] pub struct FfiPublicAccountKey { pub public_key: FfiBytes32, } /// Single entry in the account list. #[repr(C)] #[derive(Clone, Copy)] pub struct FfiAccountListEntry { pub account_id: FfiBytes32, pub is_public: bool, } /// List of accounts returned by `wallet_ffi_list_accounts`. #[repr(C)] pub struct FfiAccountList { pub entries: *mut FfiAccountListEntry, pub count: usize, } impl Default for FfiAccountList { fn default() -> Self { Self { entries: std::ptr::null_mut(), count: 0, } } } /// Result of a transfer operation. #[repr(C)] pub struct FfiTransferResult { // TODO: Replace with HashType FFI representation /// Transaction hash (null-terminated string, or null on failure). pub tx_hash: *mut c_char, /// Whether the transfer succeeded. pub success: bool, } impl Default for FfiTransferResult { fn default() -> Self { Self { tx_hash: std::ptr::null_mut(), success: false, } } } // Helper functions to convert between Rust and FFI types impl FfiBytes32 { /// Create from a 32-byte array. #[must_use] pub const fn from_bytes(bytes: [u8; 32]) -> Self { Self { data: bytes } } /// Create from an `AccountId`. #[must_use] pub const fn from_account_id(id: nssa::AccountId) -> Self { Self { data: *id.value() } } } impl From for FfiBytes32 { fn from(value: SharedSecretKey) -> Self { Self { data: value.0 } } } impl FfiPrivateAccountKeys { #[must_use] pub const fn npk(&self) -> nssa_core::NullifierPublicKey { nssa_core::NullifierPublicKey(self.nullifier_public_key.data) } pub fn vpk(&self) -> Result { if self.viewing_public_key_len == 33 { let slice = unsafe { slice::from_raw_parts(self.viewing_public_key, self.viewing_public_key_len) }; Ok(Secp256k1Point(slice.to_vec())) } else { Err(WalletFfiError::InvalidKeyValue) } } } /// 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 for FfiU128 { fn from(value: u128) -> Self { Self { data: value.to_le_bytes(), } } } impl From for u128 { fn from(value: FfiU128) -> Self { Self::from_le_bytes(value.data) } } impl From for FfiBytes32 { fn from(id: nssa::AccountId) -> Self { Self::from_account_id(id) } } impl From<[u8; 32]> for FfiBytes32 { fn from(value: [u8; 32]) -> Self { Self { data: value } } } impl From for nssa::AccountId { fn from(bytes: FfiBytes32) -> Self { Self::new(bytes.data) } } impl From for FfiAccount { #[expect( clippy::as_conversions, reason = "We need to convert to byte arrays for FFI" )] fn from(value: nssa::Account) -> Self { // Convert account data to FFI type let data_vec: Vec = value.data.into(); let data_len = data_vec.len(); let data = if data_len > 0 { let data_boxed = data_vec.into_boxed_slice(); Box::into_raw(data_boxed) as *const u8 } else { ptr::null() }; let program_owner = FfiProgramId { data: value.program_owner, }; Self { program_owner, balance: value.balance.into(), data, data_len, nonce: value.nonce.0.into(), } } } impl TryFrom<&FfiAccount> for nssa::Account { type Error = WalletFfiError; fn try_from(value: &FfiAccount) -> Result { let data = if value.data_len > 0 { unsafe { let slice = slice::from_raw_parts(value.data, value.data_len); Data::try_from(slice.to_vec()) .map_err(|_err| WalletFfiError::InvalidTypeConversion)? } } else { Data::default() }; Ok(Self { program_owner: value.program_owner.data, balance: value.balance.into(), data, nonce: nssa_core::account::Nonce(value.nonce.into()), }) } } impl From for FfiPublicAccountKey { fn from(value: nssa::PublicKey) -> Self { Self { public_key: FfiBytes32::from_bytes(*value.value()), } } } impl TryFrom<&FfiPublicAccountKey> for nssa::PublicKey { type Error = WalletFfiError; fn try_from(value: &FfiPublicAccountKey) -> Result { let public_key = Self::try_new(value.public_key.data) .map_err(|_err| WalletFfiError::InvalidTypeConversion)?; Ok(public_key) } } impl From 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 { 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); } }