diff --git a/.deny.toml b/.deny.toml index 5f3a9161..4d69e70b 100644 --- a/.deny.toml +++ b/.deny.toml @@ -60,7 +60,7 @@ allow-git = [ "https://github.com/logos-blockchain/logos-blockchain.git", "https://github.com/logos-blockchain/logos-blockchain-circuits.git", "https://github.com/logos-blockchain/logos-blockchain-rust-rapidsnark.git", - "https://github.com/arkworks-rs/spongefish.git" + "https://github.com/arkworks-rs/spongefish.git", ] unknown-git = "deny" unknown-registry = "deny" diff --git a/Cargo.lock b/Cargo.lock index d3898e49..ee5c97f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -975,9 +975,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin_hashes" -version = "0.14.100" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" dependencies = [ "hex-conservative", ] diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 24f2a9c8..ff40e5d0 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -165,6 +165,14 @@ unsafe extern "C" { fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult); + fn wallet_ffi_bridge_withdraw( + handle: *mut WalletHandle, + from: *const FfiBytes32, + amount: u64, + bedrock_account_pk: *const FfiBytes32, + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + fn wallet_ffi_register_public_account( handle: *mut WalletHandle, account_id: *const FfiBytes32, @@ -1110,6 +1118,66 @@ fn test_wallet_ffi_transfer_private() -> Result<()> { Ok(()) } +#[test] +fn test_wallet_ffi_bridge_withdraw() -> 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 bridge_account: FfiBytes32 = lee::system_bridge_account_id().into(); + let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]); + let amount = 100_u64; + + let mut transfer_result = FfiTransferResult::default(); + unsafe { + wallet_ffi_bridge_withdraw( + wallet_ffi_handle, + &raw const from, + amount, + &raw const bedrock_account_pk, + &raw mut transfer_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 bridge_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const bridge_account, + true, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(from_balance, 9900); + assert_eq!(bridge_balance, 1_000_100); + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + #[test] fn test_wallet_ffi_transfer_generic_public() -> Result<()> { let ctx = BlockingTestContext::new()?; diff --git a/lez/wallet-ffi/src/bridge.rs b/lez/wallet-ffi/src/bridge.rs new file mode 100644 index 00000000..fca71b1c --- /dev/null +++ b/lez/wallet-ffi/src/bridge.rs @@ -0,0 +1,91 @@ +//! Bridge program functions (deposit/withdraw between L1 Bedrock and L2). + +use std::{ffi::CString, ptr}; + +use lee::AccountId; +use wallet::program_facades::bridge::Bridge; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + map_execution_error, + types::{FfiBytes32, FfiTransferResult, WalletHandle}, + wallet::get_wallet, +}; + +/// Withdraw native tokens from a public account to Bedrock (L1) through the bridge. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `from`: Source public account ID (must be owned by this wallet). Bridge withdrawals only +/// support public sender accounts. +/// - `amount`: Amount of native tokens to withdraw +/// - `bedrock_account_pk`: Recipient's Bedrock (L1) public key, 32 bytes +/// - `out_result`: Output pointer for the withdraw result +/// +/// # Returns +/// - `Success` if the withdraw transaction was submitted successfully +/// - `InsufficientFunds` if the source account doesn't have enough balance +/// - `KeyNotFound` if the source account'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` +/// - `from` must be a valid pointer to a `FfiBytes32` struct +/// - `bedrock_account_pk` must be a valid pointer to a `FfiBytes32` struct +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_bridge_withdraw( + handle: *mut WalletHandle, + from: *const FfiBytes32, + amount: u64, + bedrock_account_pk: *const FfiBytes32, + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if from.is_null() || bedrock_account_pk.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 from_id = AccountId::new(unsafe { (*from).data }); + let bedrock_account_pk = unsafe { (*bedrock_account_pk).data }; + + let bridge = Bridge(&wallet); + + match block_on(bridge.send_withdraw(from_id, amount, bedrock_account_pk)) { + 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!("Bridge withdraw failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} diff --git a/lez/wallet-ffi/src/lib.rs b/lez/wallet-ffi/src/lib.rs index 6f4c1808..93a91faa 100644 --- a/lez/wallet-ffi/src/lib.rs +++ b/lez/wallet-ffi/src/lib.rs @@ -42,6 +42,7 @@ pub use types::*; use crate::error::print_error; pub mod account; +pub mod bridge; pub mod error; pub mod generic_transaction; pub mod keys; diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index 58a39b47..43e8894e 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -219,6 +219,20 @@ typedef struct FfiAccount { struct FfiU128 nonce; } FfiAccount; +/** + * Result of a transfer operation. + */ +typedef struct FfiTransferResult { + /** + * Transaction hash (null-terminated string, or null on failure). + */ + char *tx_hash; + /** + * Whether the transfer succeeded. + */ + bool success; +} FfiTransferResult; + typedef struct FfiInstructionWords { uint32_t *instruction_words; uintptr_t instruction_words_size; @@ -285,20 +299,6 @@ typedef struct FfiPublicAccountKey { struct FfiBytes32 public_key; } FfiPublicAccountKey; -/** - * Result of a transfer operation. - */ -typedef struct FfiTransferResult { - /** - * Transaction hash (null-terminated string, or null on failure). - */ - char *tx_hash; - /** - * Whether the transfer succeeded. - */ - bool success; -} FfiTransferResult; - /** * Create a new public account. * @@ -532,6 +532,38 @@ enum WalletFfiError wallet_ffi_import_private_account(struct WalletHandle *handl const struct FfiU128 *identifier, const char *account_state_json); +/** + * Withdraw native tokens from a public account to Bedrock (L1) through the bridge. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source public account ID (must be owned by this wallet). Bridge withdrawals only + * support public sender accounts. + * - `amount`: Amount of native tokens to withdraw + * - `bedrock_account_pk`: Recipient's Bedrock (L1) public key, 32 bytes + * - `out_result`: Output pointer for the withdraw result + * + * # Returns + * - `Success` if the withdraw transaction was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if the source account'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` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `bedrock_account_pk` must be a valid pointer to a `FfiBytes32` struct + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_bridge_withdraw(struct WalletHandle *handle, + const struct FfiBytes32 *from, + uint64_t amount, + const struct FfiBytes32 *bedrock_account_pk, + struct FfiTransferResult *out_result); + /** * Serialize sequence of bytes into RISC0 readable words. *