//! C-compatible type definitions for the FFI layer. use core::slice; use std::{ffi::c_char, ptr}; use nssa::Data; 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 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 FfiAccountManagerAccountIdentity #[repr(C)] pub enum FfiAccountIdentityKind { Public = 0, PublicNoSign = 1, PrivateOwned = 2, PrivateForeign = 3, PrivatePdaOwned = 4, PrivatePdaForeign = 5, PrivateShared = 6, PrivatePdaShared = 7, } /// Struct representing of account identity, given to `AccountManager` at intialization #[repr(C)] pub struct FfiAccountIdentity { 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::PrivateShared, 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( AccountIdentity::Public(value.account_id.into()), ), FfiAccountIdentityKind::PublicNoSign => Ok( AccountIdentity::PublicNoSign(value.account_id.into()), ), FfiAccountIdentityKind::PrivateOwned => Ok( AccountIdentity::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(AccountIdentity::PrivateForeign { npk: NullifierPublicKey(value.nullifier_public_key.data), vpk, identifier: value.identifier.into(), }) } FfiAccountIdentityKind::PrivatePdaOwned => Ok( AccountIdentity::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(AccountIdentity::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(AccountIdentity::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(AccountIdentity::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(), }) } } } }