From 5362a12cd16b8ab45f01fef46bd8b209c8e97816 Mon Sep 17 00:00:00 2001 From: Ricardo Guilherme Schmidt <3esmit@gmail.com> Date: Tue, 14 Jul 2026 16:57:26 -0300 Subject: [PATCH] feat: add bulk public account reads Add ordered batched account lookup across sequencer RPC, wallet, and C FFI. Bound batch size to keep worst-case responses within transport limits and expose matching FFI ownership and error contracts. Closes #617 --- Cargo.lock | 1 + integration_tests/tests/account.rs | 46 +++++++ integration_tests/tests/wallet_ffi.rs | 79 +++++++++++- lez/sequencer/service/rpc/Cargo.toml | 3 + lez/sequencer/service/rpc/src/lib.rs | 50 ++++++++ lez/sequencer/service/src/service.rs | 23 ++++ lez/wallet-ffi/cbindgen.toml | 2 +- lez/wallet-ffi/src/account.rs | 168 ++++++++++++++++++++++++-- lez/wallet-ffi/src/error.rs | 2 + lez/wallet-ffi/src/lib.rs | 8 ++ lez/wallet-ffi/src/types.rs | 16 +++ lez/wallet-ffi/wallet_ffi.h | 55 +++++++++ lez/wallet/src/lib.rs | 5 + 13 files changed, 446 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16861330..f498f092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9097,6 +9097,7 @@ version = "0.1.0" dependencies = [ "jsonrpsee", "sequencer_service_protocol", + "serde_json", ] [[package]] diff --git a/integration_tests/tests/account.rs b/integration_tests/tests/account.rs index 2b69f7e0..161b7644 100644 --- a/integration_tests/tests/account.rs +++ b/integration_tests/tests/account.rs @@ -9,6 +9,7 @@ use key_protocol::key_management::KeyChain; use lee::Data; use lee_core::account::Nonce; use log::info; +use sequencer_service_rpc::{ClientError, MAX_ACCOUNTS_PER_REQUEST, RpcClient as _}; use tokio::test; use wallet::{ account::{AccountIdWithPrivacy, HumanReadableAccount, Label}, @@ -38,6 +39,51 @@ async fn get_existing_account() -> Result<()> { Ok(()) } +#[test] +async fn get_public_accounts_returns_states_in_input_order() -> Result<()> { + let ctx = TestContext::new().await?; + let account_ids = ctx.existing_public_accounts(); + let ordered_account_ids = vec![account_ids[1], account_ids[0], account_ids[1]]; + + let empty_accounts = ctx.sequencer_client().get_accounts(Vec::new()).await?; + let single_account = ctx + .sequencer_client() + .get_accounts(vec![account_ids[0]]) + .await?; + let accounts = ctx + .sequencer_client() + .get_accounts(ordered_account_ids) + .await?; + let unknown_account = ctx + .sequencer_client() + .get_accounts(vec![lee::AccountId::new([0; 32])]) + .await?; + let oversized_error = ctx + .sequencer_client() + .get_accounts(vec![account_ids[0]; MAX_ACCOUNTS_PER_REQUEST + 1]) + .await + .expect_err("oversized account batches must be rejected"); + + assert!(empty_accounts.is_empty()); + assert_eq!(single_account, vec![accounts[1].clone()]); + assert_eq!(accounts.len(), 3); + assert_eq!(accounts[0].balance, 20_000); + assert_eq!(accounts[1].balance, 10_000); + assert_eq!(accounts[2], accounts[0]); + assert_eq!(unknown_account, vec![lee::Account::default()]); + let ClientError::Call(oversized_error) = oversized_error else { + panic!("expected an RPC call error for oversized batch") + }; + assert_eq!(oversized_error.code(), -32602); + assert!( + oversized_error + .message() + .contains("Too many accounts requested") + ); + + Ok(()) +} + #[test] async fn new_public_account_with_label() -> Result<()> { let mut ctx = TestContext::new().await?; diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 321c874d..bf9398b3 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -16,6 +16,7 @@ use std::{ ffi::{CStr, CString, c_char}, io::Write as _, path::Path, + ptr, slice, str::FromStr as _, time::Duration, }; @@ -31,9 +32,9 @@ use log::info; use tempfile::tempdir; use wallet::{account::HumanReadableAccount, program_facades::vault::Vault}; use wallet_ffi::{ - FfiAccount, FfiAccountIdWithPrivacy, FfiAccountIdentity, FfiAccountList, FfiBytes32, - FfiPrivateAccountKeys, FfiProgramId, FfiPublicAccountKey, FfiTransferResult, FfiU128, - WalletHandle, error, + FfiAccount, FfiAccountDataList, FfiAccountIdWithPrivacy, FfiAccountIdentity, FfiAccountList, + FfiBytes32, FfiPrivateAccountKeys, FfiProgramId, FfiPublicAccountKey, FfiTransferResult, + FfiU128, WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST, WalletHandle, error, generic_transaction::{FfiProgramWithDependencies, FfiTransactionResult}, label::{AccountIdResolvedFromLabel, LabelAvailability, LabelList}, wallet::FfiCreateWalletOutput, @@ -101,6 +102,13 @@ unsafe extern "C" { out_account: *mut FfiAccount, ) -> error::WalletFfiError; + fn wallet_ffi_get_accounts_public( + handle: *mut WalletHandle, + account_ids: *const FfiBytes32, + account_ids_len: usize, + out_accounts: *mut FfiAccountDataList, + ) -> error::WalletFfiError; + fn wallet_ffi_get_account_private( handle: *mut WalletHandle, account_id: *const FfiBytes32, @@ -109,6 +117,8 @@ unsafe extern "C" { fn wallet_ffi_free_account_data(account: *mut FfiAccount); + fn wallet_ffi_free_accounts_public(accounts: *mut FfiAccountDataList); + fn wallet_ffi_get_public_account_key( handle: *mut WalletHandle, account_id: *const FfiBytes32, @@ -650,6 +660,69 @@ fn test_wallet_ffi_get_account_public() -> Result<()> { Ok(()) } +#[test] +fn test_wallet_ffi_get_accounts_public() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let existing_account_ids = ctx.ctx().existing_public_accounts(); + let account_ids = [existing_account_ids[1], existing_account_ids[0]]; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let ffi_account_ids = account_ids.map(FfiBytes32::from); + let mut ffi_accounts = FfiAccountDataList::default(); + + unsafe { + wallet_ffi_get_accounts_public( + wallet_ffi_handle, + ffi_account_ids.as_ptr(), + ffi_account_ids.len(), + &raw mut ffi_accounts, + ) + .unwrap(); + } + + let accounts = unsafe { slice::from_raw_parts(ffi_accounts.accounts, ffi_accounts.count) }; + let [first, second] = accounts else { + panic!("expected two accounts, got {}", accounts.len()); + }; + assert_eq!(first.balance.data, 20_000_u128.to_le_bytes()); + assert_eq!(second.balance.data, 10_000_u128.to_le_bytes()); + + let mut empty_accounts = FfiAccountDataList::default(); + unsafe { + wallet_ffi_get_accounts_public(wallet_ffi_handle, ptr::null(), 0, &raw mut empty_accounts) + .unwrap(); + } + assert!(empty_accounts.accounts.is_null()); + assert_eq!(empty_accounts.count, 0); + + let error = unsafe { + wallet_ffi_get_accounts_public(wallet_ffi_handle, ptr::null(), 1, &raw mut empty_accounts) + }; + assert_eq!(error, error::WalletFfiError::NullPointer); + + let oversized_ids = vec![FfiBytes32::default(); WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST + 1]; + let error = unsafe { + wallet_ffi_get_accounts_public( + wallet_ffi_handle, + oversized_ids.as_ptr(), + oversized_ids.len(), + &raw mut empty_accounts, + ) + }; + assert_eq!(error, error::WalletFfiError::InvalidArgument); + + unsafe { + wallet_ffi_free_accounts_public(&raw mut ffi_accounts); + wallet_ffi_free_accounts_public(&raw mut empty_accounts); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + #[test] fn test_wallet_ffi_get_account_private() -> Result<()> { let ctx = BlockingTestContext::new()?; diff --git a/lez/sequencer/service/rpc/Cargo.toml b/lez/sequencer/service/rpc/Cargo.toml index d8f16b86..2538dc4e 100644 --- a/lez/sequencer/service/rpc/Cargo.toml +++ b/lez/sequencer/service/rpc/Cargo.toml @@ -12,6 +12,9 @@ sequencer_service_protocol.workspace = true jsonrpsee = { workspace = true, features = ["macros"] } +[dev-dependencies] +serde_json.workspace = true + [features] client = ["jsonrpsee/client"] server = ["jsonrpsee/server"] diff --git a/lez/sequencer/service/rpc/src/lib.rs b/lez/sequencer/service/rpc/src/lib.rs index f32f6bd2..0711df92 100644 --- a/lez/sequencer/service/rpc/src/lib.rs +++ b/lez/sequencer/service/rpc/src/lib.rs @@ -10,6 +10,13 @@ use sequencer_service_protocol::{ LeeTransaction, MembershipProof, Nonce, ProgramId, }; +/// Maximum number of full accounts returned by one `getAccounts` request. +/// +/// A full account may contain 100 KiB of data. JSON encodes each byte as a decimal number, so this +/// limit keeps a worst-case batch below jsonrpsee's default 10 MiB response-body limit, with at +/// least 512 KiB of headroom for the JSON-RPC envelope. +pub const MAX_ACCOUNTS_PER_REQUEST: usize = 24; + #[cfg(all(not(feature = "server"), not(feature = "client")))] compile_error!("At least one of `server` or `client` features must be enabled."); @@ -76,6 +83,16 @@ pub trait Rpc { account_ids: Vec, ) -> Result, ErrorObjectOwned>; + /// Get full account states in input order. + /// + /// Returns an invalid-params error when more than [`MAX_ACCOUNTS_PER_REQUEST`] IDs are + /// supplied. + #[method(name = "getAccounts")] + async fn get_accounts( + &self, + account_ids: Vec, + ) -> Result, ErrorObjectOwned>; + #[method(name = "getProofsAndRoot")] async fn get_proofs_and_root( &self, @@ -93,3 +110,36 @@ pub trait Rpc { // ============================================================================================= } + +#[cfg(test)] +mod tests { + use super::*; + + const MAX_ACCOUNT_DATA_SIZE: usize = 100 * 1024; + const DEFAULT_MAX_RESPONSE_BODY_SIZE: usize = 10 * 1024 * 1024; + const RESPONSE_BODY_HEADROOM: usize = 512 * 1024; + + #[test] + fn maximum_accounts_response_fits_default_response_body_limit() { + let mut account = Account { + program_owner: [u32::MAX; 8], + balance: u128::MAX, + nonce: u128::MAX.into(), + ..Account::default() + }; + account.data = vec![u8::MAX; MAX_ACCOUNT_DATA_SIZE] + .try_into() + .expect("maximum-size account data should be valid"); + + let mut encoded = br#"{"jsonrpc":"2.0","result":"#.to_vec(); + serde_json::to_writer(&mut encoded, &vec![account; MAX_ACCOUNTS_PER_REQUEST]) + .expect("response result should serialize"); + encoded.extend_from_slice(br#", "id":18446744073709551615}"#); + + assert!( + encoded.len() <= DEFAULT_MAX_RESPONSE_BODY_SIZE - RESPONSE_BODY_HEADROOM, + "maximum batch response is {} bytes", + encoded.len() + ); + } +} diff --git a/lez/sequencer/service/src/service.rs b/lez/sequencer/service/src/service.rs index d0850837..0195f5a0 100644 --- a/lez/sequencer/service/src/service.rs +++ b/lez/sequencer/service/src/service.rs @@ -15,6 +15,7 @@ use sequencer_service_protocol::{ Account, AccountId, Block, BlockId, ChannelId, Commitment, CommitmentSetDigest, HashType, MembershipProof, Nonce, ProgramId, }; +use sequencer_service_rpc::MAX_ACCOUNTS_PER_REQUEST; use tokio::sync::Mutex; const NOT_FOUND_ERROR_CODE: i32 = -31999; @@ -162,6 +163,28 @@ impl sequencer_service_rpc::RpcServer Ok(nonces) } + async fn get_accounts( + &self, + account_ids: Vec, + ) -> Result, ErrorObjectOwned> { + if account_ids.len() > MAX_ACCOUNTS_PER_REQUEST { + return Err(ErrorObjectOwned::owned( + ErrorCode::InvalidParams.code(), + format!( + "Too many accounts requested: got {}, maximum is {MAX_ACCOUNTS_PER_REQUEST}", + account_ids.len() + ), + None::<()>, + )); + } + + let sequencer = self.sequencer.lock().await; + Ok(account_ids + .into_iter() + .map(|account_id| sequencer.state().get_account_by_id(account_id)) + .collect()) + } + async fn get_proofs_and_root( &self, commitments: Vec, diff --git a/lez/wallet-ffi/cbindgen.toml b/lez/wallet-ffi/cbindgen.toml index b7824ab4..b5664524 100644 --- a/lez/wallet-ffi/cbindgen.toml +++ b/lez/wallet-ffi/cbindgen.toml @@ -28,7 +28,7 @@ include_version = true no_includes = false [export] -include = ["Ffi.*", "WalletFfiError", "WalletHandle"] +include = ["Ffi.*", "WalletFfiError", "WalletHandle", "WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST"] [enum] rename_variants = "ScreamingSnakeCase" diff --git a/lez/wallet-ffi/src/account.rs b/lez/wallet-ffi/src/account.rs index fdbfabc1..040c91d8 100644 --- a/lez/wallet-ffi/src/account.rs +++ b/lez/wallet-ffi/src/account.rs @@ -1,6 +1,6 @@ //! Account management functions. -use std::{ffi::c_char, ptr, str::FromStr as _}; +use std::{ffi::c_char, ptr, slice, str::FromStr as _}; use key_protocol::key_management::{key_tree::chain_index::ChainIndex, KeyChain}; use lee::AccountId; @@ -10,11 +10,11 @@ use crate::{ block_on, c_str_to_string, error::{print_error, WalletFfiError}, types::{ - FfiAccount, FfiAccountList, FfiAccountListEntry, FfiBytes32, FfiPrivateAccountKeys, - WalletHandle, + FfiAccount, FfiAccountDataList, FfiAccountList, FfiAccountListEntry, FfiBytes32, + FfiPrivateAccountKeys, WalletHandle, }, wallet::get_wallet, - FfiU128, + FfiU128, WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST, }; /// Create a new public account. @@ -414,6 +414,99 @@ pub unsafe extern "C" fn wallet_ffi_get_account_public( WalletFfiError::Success } +/// Get full public account data from the network in input order. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_ids`: Array of account IDs (32 bytes each); may be null when `account_ids_len` is 0 +/// - `account_ids_len`: Number of account IDs; must not exceed +/// `WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST` +/// - `out_accounts`: Output list for account data +/// +/// # Returns +/// - `Success` on successful query +/// - `InvalidArgument` when `account_ids_len` exceeds `WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST` +/// - Error code on failure +/// +/// # Memory +/// The returned list must be freed with `wallet_ffi_free_accounts_public()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `account_ids` must point to an array of `account_ids_len` `FfiBytes32` values when the length +/// is non-zero +/// - `out_accounts` must be a valid pointer to an `FfiAccountDataList` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_accounts_public( + handle: *mut WalletHandle, + account_ids: *const FfiBytes32, + account_ids_len: usize, + out_accounts: *mut FfiAccountDataList, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if out_accounts.is_null() || (account_ids_len > 0 && account_ids.is_null()) { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + if account_ids_len > WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST { + print_error(format!( + "Too many accounts requested: got {account_ids_len}, maximum is \ + {WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST}" + )); + return WalletFfiError::InvalidArgument; + } + + let account_ids = if account_ids_len == 0 { + Vec::new() + } else { + unsafe { slice::from_raw_parts(account_ids, account_ids_len) } + .iter() + .map(|account_id| AccountId::new(account_id.data)) + .collect() + }; + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let accounts = match block_on(wallet.get_accounts_public(account_ids)) { + Ok(accounts) => accounts, + Err(e) => { + print_error(format!("Failed to get accounts: {e}")); + return WalletFfiError::NetworkError; + } + }; + + let count = accounts.len(); + if count == 0 { + unsafe { + *out_accounts = FfiAccountDataList::default(); + } + } else { + let accounts = accounts + .into_iter() + .map(FfiAccount::from) + .collect::>() + .into_boxed_slice(); + let accounts = Box::into_raw(accounts).cast::(); + + unsafe { + *out_accounts = FfiAccountDataList { accounts, count }; + } + } + + WalletFfiError::Success +} + /// Get full private account data from the local storage. /// /// # Parameters @@ -481,12 +574,42 @@ pub unsafe extern "C" fn wallet_ffi_free_account_data(account: *mut FfiAccount) } unsafe { - let account = &*account; - if !account.data.is_null() && account.data_len > 0 { - let slice = std::slice::from_raw_parts_mut(account.data.cast_mut(), account.data_len); - drop(Box::from_raw(std::ptr::from_mut::<[u8]>(slice))); + free_account_data(&mut *account); + } +} + +/// Free public accounts returned by `wallet_ffi_get_accounts_public`. +/// +/// # Safety +/// The list must be either null or a valid list returned by +/// `wallet_ffi_get_accounts_public`. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_accounts_public(accounts: *mut FfiAccountDataList) { + if accounts.is_null() { + return; + } + + unsafe { + let accounts = &mut *accounts; + if !accounts.accounts.is_null() && accounts.count > 0 { + let list = slice::from_raw_parts_mut(accounts.accounts, accounts.count); + for account in list.iter_mut() { + free_account_data(account); + } + drop(Box::from_raw(std::ptr::from_mut::<[FfiAccount]>(list))); + } + *accounts = FfiAccountDataList::default(); + } +} + +fn free_account_data(account: &mut FfiAccount) { + if !account.data.is_null() && account.data_len > 0 { + unsafe { + let data = slice::from_raw_parts_mut(account.data.cast_mut(), account.data_len); + drop(Box::from_raw(std::ptr::from_mut::<[u8]>(data))); } } + *account = FfiAccount::default(); } /// Import a public account private key into wallet storage. @@ -653,3 +776,32 @@ pub unsafe extern "C" fn wallet_ffi_import_private_account( } } } + +#[cfg(test)] +mod tests { + use lee::{Account, Data}; + + use super::*; + + #[test] + fn free_accounts_public_releases_nested_account_data() { + let expected_data = vec![1, 2, 3, 4]; + let account = Account { + data: Data::try_from(expected_data.clone()).expect("account data should be valid"), + ..Account::default() + }; + let accounts = vec![FfiAccount::from(account)].into_boxed_slice(); + let count = accounts.len(); + let accounts = Box::into_raw(accounts).cast::(); + let mut list = FfiAccountDataList { accounts, count }; + + let data = unsafe { slice::from_raw_parts((*list.accounts).data, expected_data.len()) }; + assert_eq!(data, expected_data); + + unsafe { + wallet_ffi_free_accounts_public(&raw mut list); + } + assert!(list.accounts.is_null()); + assert_eq!(list.count, 0); + } +} diff --git a/lez/wallet-ffi/src/error.rs b/lez/wallet-ffi/src/error.rs index c701cb6f..15e972ef 100644 --- a/lez/wallet-ffi/src/error.rs +++ b/lez/wallet-ffi/src/error.rs @@ -45,6 +45,8 @@ pub enum WalletFfiError { InvalidKeyValue = 16, /// Invalid program bytecode. InvalidBytecode = 17, + /// An argument is outside the supported range. + InvalidArgument = 18, /// Internal error (catch-all). InternalError = 99, } diff --git a/lez/wallet-ffi/src/lib.rs b/lez/wallet-ffi/src/lib.rs index c91185e6..77244b0f 100644 --- a/lez/wallet-ffi/src/lib.rs +++ b/lez/wallet-ffi/src/lib.rs @@ -56,6 +56,14 @@ pub mod types; pub mod vault; pub mod wallet; +/// Maximum number of account IDs accepted by `wallet_ffi_get_accounts_public`. +pub const WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST: usize = 24; + +const _: () = assert!( + WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST == sequencer_service_rpc::MAX_ACCOUNTS_PER_REQUEST, + "wallet FFI and sequencer RPC account batch limits must match" +); + static TOKIO_RUNTIME: OnceLock = OnceLock::new(); /// Get a reference to the global runtime. diff --git a/lez/wallet-ffi/src/types.rs b/lez/wallet-ffi/src/types.rs index 3779ba01..8db0e683 100644 --- a/lez/wallet-ffi/src/types.rs +++ b/lez/wallet-ffi/src/types.rs @@ -102,6 +102,22 @@ impl Default for FfiAccount { } } +/// List of full account data returned by `wallet_ffi_get_accounts_public`. +#[repr(C)] +pub struct FfiAccountDataList { + pub accounts: *mut FfiAccount, + pub count: usize, +} + +impl Default for FfiAccountDataList { + fn default() -> Self { + Self { + accounts: std::ptr::null_mut(), + count: 0, + } + } +} + /// Public keys for a private account (safe to expose). #[repr(C)] pub struct FfiPrivateAccountKeys { diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index 4104fb61..1cd221fb 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -31,6 +31,11 @@ #include #include +/** + * Maximum number of account IDs accepted by `wallet_ffi_get_accounts_public`. + */ +#define WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST 24 + /** * Error codes returned by FFI functions. */ @@ -107,6 +112,10 @@ typedef enum WalletFfiError { * Invalid program bytecode. */ INVALID_BYTECODE = 17, + /** + * An argument is outside the supported range. + */ + INVALID_ARGUMENT = 18, /** * Internal error (catch-all). */ @@ -219,6 +228,14 @@ typedef struct FfiAccount { struct FfiU128 nonce; } FfiAccount; +/** + * List of full account data returned by `wallet_ffi_get_accounts_public`. + */ +typedef struct FfiAccountDataList { + struct FfiAccount *accounts; + uintptr_t count; +} FfiAccountDataList; + /** * Result of a transfer operation. */ @@ -488,6 +505,35 @@ enum WalletFfiError wallet_ffi_get_account_public(struct WalletHandle *handle, const struct FfiBytes32 *account_id, struct FfiAccount *out_account); +/** + * Get full public account data from the network in input order. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_ids`: Array of account IDs (32 bytes each); may be null when `account_ids_len` is 0 + * - `account_ids_len`: Number of account IDs; must not exceed + * `WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST` + * - `out_accounts`: Output list for account data + * + * # Returns + * - `Success` on successful query + * - `InvalidArgument` when `account_ids_len` exceeds `WALLET_FFI_MAX_ACCOUNTS_PER_REQUEST` + * - Error code on failure + * + * # Memory + * The returned list must be freed with `wallet_ffi_free_accounts_public()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `account_ids` must point to an array of `account_ids_len` `FfiBytes32` values when the length + * is non-zero + * - `out_accounts` must be a valid pointer to an `FfiAccountDataList` struct + */ +enum WalletFfiError wallet_ffi_get_accounts_public(struct WalletHandle *handle, + const struct FfiBytes32 *account_ids, + uintptr_t account_ids_len, + struct FfiAccountDataList *out_accounts); + /** * Get full private account data from the local storage. * @@ -521,6 +567,15 @@ enum WalletFfiError wallet_ffi_get_account_private(struct WalletHandle *handle, */ void wallet_ffi_free_account_data(struct FfiAccount *account); +/** + * Free public accounts returned by `wallet_ffi_get_accounts_public`. + * + * # Safety + * The list must be either null or a valid list returned by + * `wallet_ffi_get_accounts_public`. + */ +void wallet_ffi_free_accounts_public(struct FfiAccountDataList *accounts); + /** * Import a public account private key into wallet storage. * diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index fdd208af..815e0a25 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -424,6 +424,11 @@ impl WalletCore { Ok(self.sequencer_client.get_accounts_nonces(accs).await?) } + /// Get public accounts from the sequencer in input order. + pub async fn get_accounts_public(&self, account_ids: Vec) -> Result> { + Ok(self.sequencer_client.get_accounts(account_ids).await?) + } + pub async fn get_account(&self, account_id: AccountIdWithPrivacy) -> Result { match account_id { AccountIdWithPrivacy::Public(acc_id) => self.get_account_public(acc_id).await,