Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions contracts/invoice-escrow/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
35 changes: 35 additions & 0 deletions contracts/invoice-escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<soroban_sdk::Vec<EscrowData>, 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)]
Expand Down
27 changes: 27 additions & 0 deletions contracts/invoice-escrow/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Symbol> {
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);
}
86 changes: 86 additions & 0 deletions contracts/invoice-escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
4 changes: 4 additions & 0 deletions contracts/invoice-escrow/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down