feat(wallet-ffi): add vault claim ffi

This commit is contained in:
Sergio Chouhy 2026-06-20 17:24:37 -03:00 committed by moudyellaz
parent ec11c2ab0b
commit 47a94ac7e4
6 changed files with 493 additions and 1 deletions

1
Cargo.lock generated
View File

@ -10775,6 +10775,7 @@ dependencies = [
"serde_json",
"tempfile",
"tokio",
"vault_core",
"wallet",
]

View File

@ -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(())
}

View File

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

View File

@ -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<tokio::runtime::Runtime> = OnceLock::new();

219
lez/wallet-ffi/src/vault.rs Normal file
View File

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

View File

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