diff --git a/contracts/invoice-escrow/src/errors.rs b/contracts/invoice-escrow/src/errors.rs index 7a84e30..d29f1af 100644 --- a/contracts/invoice-escrow/src/errors.rs +++ b/contracts/invoice-escrow/src/errors.rs @@ -40,4 +40,8 @@ pub enum Error { InvalidPayer = 15, /// Due date is invalid (e.g., in the past or zero). InvalidDueDate = 16, + /// Limit is zero or invalid for batch query. + InvalidLimit = 17, + /// Limit exceeds the maximum allowed page size. + LimitExceeded = 18, } diff --git a/contracts/invoice-escrow/src/lib.rs b/contracts/invoice-escrow/src/lib.rs index cddf3b7..ff4f6eb 100644 --- a/contracts/invoice-escrow/src/lib.rs +++ b/contracts/invoice-escrow/src/lib.rs @@ -21,6 +21,7 @@ use errors::Error; const MAX_BPS: u32 = 10_000; const DISTRIBUTE_PAYMENT_FN: &str = "distribute_payment"; const DISTRIBUTE_REFUND_FN: &str = "distribute_refund"; +const MAX_PAGE_SIZE: u32 = 100; #[contract] pub struct InvoiceEscrow; @@ -99,6 +100,11 @@ impl InvoiceEscrow { commitment: commitment.clone(), }; storage::set_escrow(&env, invoice_id.clone(), &data); + + let index = storage::get_escrow_count(&env); + storage::set_escrow_id_by_index(&env, index, invoice_id.clone()); + storage::set_escrow_count(&env, index.checked_add(1).ok_or(Error::Overflow)?); + events::escrow_created( &env, invoice_id, @@ -481,6 +487,35 @@ impl InvoiceEscrow { let config = storage::get_config(&env).ok_or(Error::NotInit)?; Ok(config.paused) } + + /// View: return a batch of escrows with pagination. + /// Rejects limit = 0 and limits > MAX_PAGE_SIZE. + pub fn get_escrows(env: Env, start: u32, limit: u32) -> Result, Error> { + if limit == 0 { + return Err(Error::InvalidLimit); + } + if limit > MAX_PAGE_SIZE { + return Err(Error::LimitExceeded); + } + + let total_count = storage::get_escrow_count(&env); + let mut result = soroban_sdk::Vec::new(&env); + + if start >= total_count { + return Ok(result); + } + + let end = start.checked_add(limit).unwrap_or(total_count).min(total_count); + + for i in start..end { + if let Some(id) = storage::get_escrow_id_by_index(&env, i) { + if let Some(escrow) = storage::get_escrow(&env, id) { + result.push_back(escrow); + } + } + } + Ok(result) + } } #[cfg(test)] diff --git a/contracts/invoice-escrow/src/storage.rs b/contracts/invoice-escrow/src/storage.rs index 050381c..9cadce9 100644 --- a/contracts/invoice-escrow/src/storage.rs +++ b/contracts/invoice-escrow/src/storage.rs @@ -60,3 +60,30 @@ pub fn set_funder_amount( .set(&StorageKey::FunderAmount(inv_id, funder.clone()), &amount); } } + +/// Get the total number of escrows. +pub fn get_escrow_count(env: &soroban_sdk::Env) -> u32 { + env.storage() + .persistent() + .get(&StorageKey::EscrowCount) + .unwrap_or(0) +} + +/// Set the total number of escrows. +pub fn set_escrow_count(env: &soroban_sdk::Env, count: u32) { + env.storage().persistent().set(&StorageKey::EscrowCount, &count); +} + +/// Get an escrow ID by its index (0-based). +pub fn get_escrow_id_by_index(env: &soroban_sdk::Env, index: u32) -> Option { + env.storage() + .persistent() + .get(&StorageKey::EscrowIdByIndex(index)) +} + +/// Set an escrow ID at the given index. +pub fn set_escrow_id_by_index(env: &soroban_sdk::Env, index: u32, inv_id: Symbol) { + env.storage() + .persistent() + .set(&StorageKey::EscrowIdByIndex(index), &inv_id); +} diff --git a/contracts/invoice-escrow/src/test.rs b/contracts/invoice-escrow/src/test.rs index 944b2d0..f7269a1 100644 --- a/contracts/invoice-escrow/src/test.rs +++ b/contracts/invoice-escrow/src/test.rs @@ -2229,3 +2229,89 @@ fn test_create_escrow_due_date_in_future_accepted() { assert_eq!(escrow_data.due_dt, future_due_date); assert_eq!(escrow_data.status, EscrowStatus::Created); } + +#[test] +fn test_get_escrows_empty() { + let env = Env::default(); + let escrow_id = env.register_contract(None, InvoiceEscrow); + let escrow_client = InvoiceEscrowClient::new(&env, &escrow_id); + + let escrows = escrow_client.get_escrows(&0, &10); + assert_eq!(escrows.len(), 0); +} + +#[test] +fn test_get_escrows_pagination() { + let env = Env::default(); + env.mock_all_auths(); + let escrow_id = env.register_contract(None, InvoiceEscrow); + let escrow_client = InvoiceEscrowClient::new(&env, &escrow_id); + + let admin = Address::generate(&env); + let payment_token = Address::generate(&env); + let inv_token = Address::generate(&env); + escrow_client.initialize(&admin, &300); + + let seller = Address::generate(&env); + env.ledger().with_mut(|li| li.timestamp = 1000000); + let due_date = env.ledger().timestamp() + 1000000; + + let ids = ["INV0", "INV1", "INV2", "INV3", "INV4"]; + for id_str in ids.iter() { + let invoice_id = Symbol::new(&env, id_str); + escrow_client.create_escrow( + &invoice_id, + &seller, + &seller, + &1000, + &1000, + &due_date, + &payment_token, + &inv_token, + &test_commitment(&env, "test_data"), + ); + } + + let page1 = escrow_client.get_escrows(&0, &3); + assert_eq!(page1.len(), 3); + assert_eq!(page1.get(0).unwrap().inv_id, Symbol::new(&env, "INV0")); + assert_eq!(page1.get(2).unwrap().inv_id, Symbol::new(&env, "INV2")); + + let page2 = escrow_client.get_escrows(&3, &3); + assert_eq!(page2.len(), 2); + assert_eq!(page2.get(0).unwrap().inv_id, Symbol::new(&env, "INV3")); + assert_eq!(page2.get(1).unwrap().inv_id, Symbol::new(&env, "INV4")); + + let page3 = escrow_client.get_escrows(&10, &3); + assert_eq!(page3.len(), 0); + + let page4 = escrow_client.get_escrows(&0, &5); + assert_eq!(page4.len(), 5); +} + +#[test] +#[should_panic(expected = "Error(Contract, 17)")] +fn test_get_escrows_invalid_limit() { + let env = Env::default(); + let escrow_id = env.register_contract(None, InvoiceEscrow); + let escrow_client = InvoiceEscrowClient::new(&env, &escrow_id); + escrow_client.get_escrows(&0, &0); +} + +#[test] +#[should_panic(expected = "Error(Contract, 18)")] +fn test_get_escrows_limit_exceeded() { + let env = Env::default(); + let escrow_id = env.register_contract(None, InvoiceEscrow); + let escrow_client = InvoiceEscrowClient::new(&env, &escrow_id); + escrow_client.get_escrows(&0, &101); +} + +#[test] +fn test_get_escrows_max_page_size() { + let env = Env::default(); + let escrow_id = env.register_contract(None, InvoiceEscrow); + let escrow_client = InvoiceEscrowClient::new(&env, &escrow_id); + let page = escrow_client.get_escrows(&0, &100); + assert_eq!(page.len(), 0); +} diff --git a/contracts/invoice-escrow/src/types.rs b/contracts/invoice-escrow/src/types.rs index 7c62d73..522bf5f 100644 --- a/contracts/invoice-escrow/src/types.rs +++ b/contracts/invoice-escrow/src/types.rs @@ -13,6 +13,10 @@ pub enum StorageKey { Escrow(soroban_sdk::Symbol), /// Persistent: funder amounts by (invoice_id, funder_address). FunderAmount(soroban_sdk::Symbol, soroban_sdk::Address), + /// Persistent: total number of escrows created. + EscrowCount, + /// Persistent: escrow invoice id by index (u32). + EscrowIdByIndex(u32), } /// Global contract configuration.