From 47a94ac7e45493d9eadd964610652ca701d57a44 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Sat, 20 Jun 2026 17:24:37 -0300 Subject: [PATCH] feat(wallet-ffi): add vault claim ffi --- Cargo.lock | 1 + integration_tests/tests/wallet_ffi.rs | 193 ++++++++++++++++++++++- lez/wallet-ffi/Cargo.toml | 1 + lez/wallet-ffi/src/lib.rs | 1 + lez/wallet-ffi/src/vault.rs | 219 ++++++++++++++++++++++++++ lez/wallet-ffi/wallet_ffi.h | 79 ++++++++++ 6 files changed, 493 insertions(+), 1 deletion(-) create mode 100644 lez/wallet-ffi/src/vault.rs diff --git a/Cargo.lock b/Cargo.lock index 3a4db53d..214f94de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10775,6 +10775,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", + "vault_core", "wallet", ] diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index ff40e5d0..ad63e9b7 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -28,7 +28,7 @@ use lee::{ use lee_core::program::DEFAULT_PROGRAM_ID; use log::info; use tempfile::tempdir; -use wallet::account::HumanReadableAccount; +use wallet::{account::HumanReadableAccount, program_facades::vault::Vault}; use wallet_ffi::{ FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error, @@ -173,6 +173,26 @@ unsafe extern "C" { out_result: *mut FfiTransferResult, ) -> error::WalletFfiError; + fn wallet_ffi_get_vault_balance( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + out_balance: *mut [u8; 16], + ) -> error::WalletFfiError; + + fn wallet_ffi_vault_claim( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + + fn wallet_ffi_vault_claim_private( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + fn wallet_ffi_register_public_account( handle: *mut WalletHandle, account_id: *const FfiBytes32, @@ -1375,3 +1395,174 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> { Ok(()) } + +#[test] +fn test_wallet_ffi_vault_balance_and_claim_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 sender = ctx.ctx().existing_public_accounts()[0]; + let owner = ctx.ctx().existing_public_accounts()[1]; + let owner_ffi: FfiBytes32 = owner.into(); + let amount: u128 = 100; + + // Fund the owner's vault, simulating an L1 bridge deposit. + ctx.block_on(|ctx| async move { + Vault(ctx.wallet()) + .send_transfer(sender, owner, amount) + .await + }) + .unwrap(); + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let vault_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance, amount); + + let mut transfer_result = FfiTransferResult::default(); + let claim_amount: [u8; 16] = amount.to_le_bytes(); + unsafe { + wallet_ffi_vault_claim( + wallet_ffi_handle, + &raw const owner_ffi, + &raw const claim_amount, + &raw mut transfer_result, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let vault_balance_after_claim = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance_after_claim, 0); + + let owner_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const owner_ffi, + true, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(owner_balance, 20_000 + amount); + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_vault_balance_and_claim_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 sender = ctx.ctx().existing_public_accounts()[0]; + let owner = ctx.ctx().existing_private_accounts()[0]; + let owner_ffi: FfiBytes32 = owner.into(); + let amount: u128 = 100; + + // Fund the owner's vault. Real deposits always land via a public transfer (the bridge + // program crediting the vault PDA), regardless of whether the owner is private. + ctx.block_on(|ctx| async move { + Vault(ctx.wallet()) + .send_transfer(sender, owner, amount) + .await + }) + .unwrap(); + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let vault_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance, amount); + + let mut transfer_result = FfiTransferResult::default(); + let claim_amount: [u8; 16] = amount.to_le_bytes(); + unsafe { + wallet_ffi_vault_claim_private( + wallet_ffi_handle, + &raw const owner_ffi, + &raw const claim_amount, + &raw mut transfer_result, + ) + .unwrap(); + } + + 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 vault_balance_after_claim = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance_after_claim, 0); + + let owner_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const owner_ffi, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + assert_eq!(owner_balance, 10_000 + amount); + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} diff --git a/lez/wallet-ffi/Cargo.toml b/lez/wallet-ffi/Cargo.toml index 1e8b6395..43a4f05d 100644 --- a/lez/wallet-ffi/Cargo.toml +++ b/lez/wallet-ffi/Cargo.toml @@ -21,6 +21,7 @@ tokio.workspace = true key_protocol.workspace = true serde_json.workspace = true risc0-zkvm.workspace = true +vault_core.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/lez/wallet-ffi/src/lib.rs b/lez/wallet-ffi/src/lib.rs index 93a91faa..6c86b0c8 100644 --- a/lez/wallet-ffi/src/lib.rs +++ b/lez/wallet-ffi/src/lib.rs @@ -51,6 +51,7 @@ pub mod program_deployment; pub mod sync; pub mod transfer; pub mod types; +pub mod vault; pub mod wallet; static TOKIO_RUNTIME: OnceLock = OnceLock::new(); diff --git a/lez/wallet-ffi/src/vault.rs b/lez/wallet-ffi/src/vault.rs new file mode 100644 index 00000000..e0575e3d --- /dev/null +++ b/lez/wallet-ffi/src/vault.rs @@ -0,0 +1,219 @@ +//! Bridge vault claim functions. +//! +//! L1 Bedrock deposits are minted into a per-owner vault PDA account by the bridge program, not +//! directly into the owner's account (see `bridge.rs`). The owner must separately claim the +//! deposited funds from their vault into their own account with a signed transaction. + +use std::{ffi::CString, ptr}; + +use lee::{program::Program, AccountId}; +use wallet::program_facades::vault::Vault; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + map_execution_error, + types::{FfiBytes32, FfiTransferResult, WalletHandle}, + wallet::get_wallet, +}; + +/// Get the claimable balance held in an account's bridge vault. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `owner`: The account ID whose vault balance to query +/// - `out_balance`: Output for balance as little-endian [u8; 16] +/// +/// # Returns +/// - `Success` on successful query +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `owner` must be a valid pointer to a `FfiBytes32` struct +/// - `out_balance` must be a valid pointer to a `[u8; 16]` array +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_vault_balance( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + out_balance: *mut [u8; 16], +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if owner.is_null() || out_balance.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 owner_id = AccountId::new(unsafe { (*owner).data }); + let vault_id = vault_core::compute_vault_account_id(Program::vault().id(), owner_id); + + let balance = match block_on(wallet.get_account_balance(vault_id)) { + Ok(b) => b, + Err(e) => { + print_error(format!("Failed to get vault balance: {e}")); + return WalletFfiError::NetworkError; + } + }; + + unsafe { + *out_balance = balance.to_le_bytes(); + } + + WalletFfiError::Success +} + +/// Claim native tokens from a public owner's vault into their account. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `owner`: Owner account ID (must be owned by this wallet, public) +/// - `amount`: Amount to claim as little-endian [u8; 16] +/// - `out_result`: Output pointer for the claim result +/// +/// # Returns +/// - `Success` if the claim was submitted successfully +/// - `InsufficientFunds` if the vault doesn't have enough balance +/// - `KeyNotFound` if the owner's signing key is not in this wallet +/// - Error code on other failures +/// +/// # 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` +/// - `owner` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` 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_vault_claim( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if owner.is_null() || amount.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 owner_id = AccountId::new(unsafe { (*owner).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + match block_on(Vault(&wallet).send_claim(owner_id, amount)) { + Ok(tx_hash) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(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!("Vault claim failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} + +/// Claim native tokens from a private owner's vault into their account. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `owner`: Owner account ID (must be owned by this wallet, private) +/// - `amount`: Amount to claim as little-endian [u8; 16] +/// - `out_result`: Output pointer for the claim result +/// +/// # Returns +/// - `Success` if the claim was submitted successfully +/// - `InsufficientFunds` if the vault doesn't have enough balance +/// - `KeyNotFound` if the owner's signing key is not in this wallet +/// - Error code on other failures +/// +/// # 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` +/// - `owner` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` 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_vault_claim_private( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if owner.is_null() || amount.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 owner_id = AccountId::new(unsafe { (*owner).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + match block_on(Vault(&wallet).send_claim_private_owner(owner_id, amount)) { + Ok((tx_hash, _shared_key)) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(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!("Vault claim failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index 43e8894e..83f30d5d 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -1361,6 +1361,85 @@ enum WalletFfiError wallet_ffi_register_private_account(struct WalletHandle *han */ void wallet_ffi_free_transfer_result(struct FfiTransferResult *result); +/** + * Get the claimable balance held in an account's bridge vault. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `owner`: The account ID whose vault balance to query + * - `out_balance`: Output for balance as little-endian [u8; 16] + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `owner` must be a valid pointer to a `FfiBytes32` struct + * - `out_balance` must be a valid pointer to a `[u8; 16]` array + */ +enum WalletFfiError wallet_ffi_get_vault_balance(struct WalletHandle *handle, + const struct FfiBytes32 *owner, + uint8_t (*out_balance)[16]); + +/** + * Claim native tokens from a public owner's vault into their account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `owner`: Owner account ID (must be owned by this wallet, public) + * - `amount`: Amount to claim as little-endian [u8; 16] + * - `out_result`: Output pointer for the claim result + * + * # Returns + * - `Success` if the claim was submitted successfully + * - `InsufficientFunds` if the vault doesn't have enough balance + * - `KeyNotFound` if the owner's signing key is not in this wallet + * - Error code on other failures + * + * # 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` + * - `owner` must be a valid pointer to a `FfiBytes32` struct + * - `amount` 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_vault_claim(struct WalletHandle *handle, + const struct FfiBytes32 *owner, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + +/** + * Claim native tokens from a private owner's vault into their account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `owner`: Owner account ID (must be owned by this wallet, private) + * - `amount`: Amount to claim as little-endian [u8; 16] + * - `out_result`: Output pointer for the claim result + * + * # Returns + * - `Success` if the claim was submitted successfully + * - `InsufficientFunds` if the vault doesn't have enough balance + * - `KeyNotFound` if the owner's signing key is not in this wallet + * - Error code on other failures + * + * # 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` + * - `owner` must be a valid pointer to a `FfiBytes32` struct + * - `amount` 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_vault_claim_private(struct WalletHandle *handle, + const struct FfiBytes32 *owner, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + /** * Create a new wallet with fresh storage. *