From 723c00c3964df072b52bdda059b003785cb0093c Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:11:20 +0100 Subject: [PATCH 1/6] feat(contract): add creator and payer invoice total views --- contracts/sharpy/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs index f45c4e3..1eb01e4 100644 --- a/contracts/sharpy/src/lib.rs +++ b/contracts/sharpy/src/lib.rs @@ -846,6 +846,19 @@ impl SharpyContract { paginate_ids(&env, ids, limit, offset) } + /// Total invoices created by `creator` (matches `get_invoices_by_creator(...).len()`). + /// O(1) read for dashboards: fetch the total once, then page with + /// `get_creator_invoices_paged` until a short page arrives. + pub fn get_creator_invoice_total(env: Env, creator: Address) -> u32 { + env.storage().persistent().get::<(Symbol, Address), Vec>(&creator_index_key(&creator)).map(|v| v.len()).unwrap_or(0) + } + + /// Total invoices paid by `payer` (matches `get_invoices_by_payer(...).len()`). + /// Pairs with `get_payer_invoices_paged` the same way the creator total does. + pub fn get_payer_invoice_total(env: Env, payer: Address) -> u32 { + env.storage().persistent().get::<(Symbol, Address), Vec>(&payer_index_key(&payer)).map(|v| v.len()).unwrap_or(0) + } + /// Returns the claimable balance for a given account and token. /// Balances accumulate when recipient transfers fail during invoice release. /// Returns 0 if no balance exists. From 18066d55257676d9dfcf9b8f53d472d0afc639ed Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:11:20 +0100 Subject: [PATCH 2/6] docs(contract): link paged queries to total views --- contracts/sharpy/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs index 1eb01e4..aa2206f 100644 --- a/contracts/sharpy/src/lib.rs +++ b/contracts/sharpy/src/lib.rs @@ -832,6 +832,7 @@ impl SharpyContract { } /// Returns one page of invoice IDs created by `creator`. + /// Pair with `get_creator_invoice_total` for total-aware paging. /// `limit` caps the page size (0 yields an empty page); `offset` skips that /// many entries (past-the-end yields an empty page). Index order is creation /// order. The unpaginated `get_invoices_by_creator` is unchanged. From bb1c4622fbce5a9408b44bf30e1701f94ac6bea1 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:11:27 +0100 Subject: [PATCH 3/6] test(contract): add paged unknown and total-match cases --- contracts/sharpy/src/test.rs | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 324f5e6..3f0fda7 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -5883,3 +5883,45 @@ mod test_fee_consistency_c { assert_eq!(client.preview_fee(&10_000i128), 100i128); } } + +#[cfg(test)] +mod test_paged_total_a { + use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; + use crate::SharpyContractClient; + fn setup() -> (Env, SharpyContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let cid = env.register(crate::SharpyContract, ()); + let c = SharpyContractClient::new(&env, &cid); + let a = Address::generate(&env); + let t = Address::generate(&env); + c.initialize(&a, &t); + (env, c) + } + fn mk(env: &Env, client: &SharpyContractClient<'_>, creator: &Address) -> u64 { + let r = Address::generate(env); + let tok = Address::generate(env); + let dl = env.ledger().timestamp() + 86400; + let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(env), auto_resolve_rules: Vec::new(env), arbitrator: None }; + client.create_invoice(creator, &Vec::from_array(env, [r]), &Vec::from_array(env, [100i128]), &Vec::from_array(env, [tok]), &dl, &opts) + } + #[test] + fn test_unknown_totals_zero_and_empty() { + let (env, client) = setup(); + let ghost = Address::generate(&env); + assert_eq!(client.get_creator_invoice_total(&ghost), 0u32); + assert_eq!(client.get_payer_invoice_total(&ghost), 0u32); + assert_eq!(client.get_creator_invoices_paged(&ghost, &10u32, &0u32).len(), 0u32); + assert_eq!(client.get_payer_invoices_paged(&ghost, &10u32, &0u32).len(), 0u32); + } + #[test] + fn test_totals_match_unpaginated_len() { + let (env, client) = setup(); + let creator = Address::generate(&env); + mk(&env, &client, &creator); + mk(&env, &client, &creator); + mk(&env, &client, &creator); + assert_eq!(client.get_invoices_by_creator(&creator).len(), 3u32); + assert_eq!(client.get_creator_invoice_total(&creator), 3u32); + } +} From a9b114404bc12caa5e43d68f6d577f63ed130b6d Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:11:27 +0100 Subject: [PATCH 4/6] test(contract): add paged empty-page and payer-total cases --- contracts/sharpy/src/test.rs | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 3f0fda7..4acbd5d 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -5925,3 +5925,52 @@ mod test_paged_total_a { assert_eq!(client.get_creator_invoice_total(&creator), 3u32); } } + +#[cfg(test)] +mod test_paged_total_b { + use soroban_sdk::{testutils::Address as _, token, Address, Env, Vec}; + use crate::{types::InvoicePayment, SharpyContractClient}; + fn setup() -> (Env, SharpyContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let cid = env.register(crate::SharpyContract, ()); + let c = SharpyContractClient::new(&env, &cid); + let a = Address::generate(&env); + let t = Address::generate(&env); + c.initialize(&a, &t); + (env, c) + } + #[test] + fn test_limit_zero_and_past_end_empty() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let r = Address::generate(&env); + let tok = Address::generate(&env); + let dl = env.ledger().timestamp() + 86400; + let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(&env), auto_resolve_rules: Vec::new(&env), arbitrator: None }; + let id = client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok]), &dl, &opts); + assert_eq!(client.get_creator_invoices_paged(&creator, &0u32, &0u32).len(), 0u32); + assert_eq!(client.get_creator_invoices_paged(&creator, &10u32, &99u32).len(), 0u32); + assert_eq!(client.get_creator_invoice_total(&creator), 1u32); + let _ = id; + } + #[test] + fn test_payer_total_tracks_payments() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let payer = Address::generate(&env); + let admin = Address::generate(&env); + let tok = env.register_stellar_asset_contract(admin); + token::StellarAssetClient::new(&env, &tok).mint(&payer, &10_000i128); + let dl = env.ledger().timestamp() + 86400; + let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(&env), auto_resolve_rules: Vec::new(&env), arbitrator: None }; + for _ in 0..2 { + let r = Address::generate(&env); + let id = client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [tok.clone()]), &dl, &opts); + let p = InvoicePayment { invoice_id: id, amount: 50i128 }; + client.pool_pay(&payer, &Vec::from_array(&env, [p])); + } + assert_eq!(client.get_payer_invoice_total(&payer), 2u32); + assert_eq!(client.get_invoices_by_payer(&payer).len(), 2u32); + } +} From 2463397e23b2afa0687fb31e309ba8f48c0c1e92 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:11:31 +0100 Subject: [PATCH 5/6] test(contract): add paged walk and truncation cases --- contracts/sharpy/src/test.rs | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 4acbd5d..4d99d49 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -5974,3 +5974,52 @@ mod test_paged_total_b { assert_eq!(client.get_invoices_by_payer(&payer).len(), 2u32); } } + +#[cfg(test)] +mod test_paged_total_c { + use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; + use crate::SharpyContractClient; + fn setup() -> (Env, SharpyContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let cid = env.register(crate::SharpyContract, ()); + let c = SharpyContractClient::new(&env, &cid); + let a = Address::generate(&env); + let t = Address::generate(&env); + c.initialize(&a, &t); + (env, c) + } + fn mk(env: &Env, client: &SharpyContractClient<'_>, creator: &Address) -> u64 { + let r = Address::generate(env); + let tok = Address::generate(env); + let dl = env.ledger().timestamp() + 86400; + let opts = crate::types::InvoiceOptions { escrow_enabled: false, escrow_release_delay: None, split_rules: Vec::new(env), auto_resolve_rules: Vec::new(env), arbitrator: None }; + client.create_invoice(creator, &Vec::from_array(env, [r]), &Vec::from_array(env, [100i128]), &Vec::from_array(env, [tok]), &dl, &opts) + } + #[test] + fn test_paging_walk_reconstructs_list() { + let (env, client) = setup(); + let creator = Address::generate(&env); + for _ in 0..5 { + mk(&env, &client, &creator); + } + let total = client.get_creator_invoice_total(&creator); + assert_eq!(total, 5u32); + let p0 = client.get_creator_invoices_paged(&creator, &2u32, &0u32); + let p1 = client.get_creator_invoices_paged(&creator, &2u32, &2u32); + let p2 = client.get_creator_invoices_paged(&creator, &2u32, &4u32); + assert_eq!(p0.len() + p1.len() + p2.len(), total); + let full = client.get_invoices_by_creator(&creator); + assert_eq!(p0.get(0).unwrap(), full.get(0).unwrap()); + assert_eq!(p2.get(0).unwrap(), full.get(4).unwrap()); + } + #[test] + fn test_truncation_returns_short_page() { + let (env, client) = setup(); + let creator = Address::generate(&env); + mk(&env, &client, &creator); + mk(&env, &client, &creator); + let page = client.get_creator_invoices_paged(&creator, &10u32, &1u32); + assert_eq!(page.len(), 1u32); + } +} From 9fa83b98fd2c069db757e518bcd28066c8eaff19 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:11:57 +0100 Subject: [PATCH 6/6] docs(changelog): record paged totals for #196 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d79e819..614fbfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the Sharpy smart contract are documented here. ## [Unreleased] +- feat(contract): `get_creator_invoice_total` + `get_payer_invoice_total` O(1) totals; empty-page/total consistency proof — feat/paged-totals (closes #196) - feat(contract): `get_fee_bps()` rate view + preview-vs-invoice consistency proof (emitting vs silent share math, release untouched) — feat/fee-consistency (closes #195) - feat(contract): whitelist enforced uniformly in `pay`/`pay_with_tip`/`pool_pay` + `is_whitelisted_payer(id, payer)` view; `require!` message consistency — feat/whitelist-consistency (closes #194) - feat(contract): `get_tranche_remaining_bps(id)` view + cumulative-bps invariant (`released + remaining == 10000`) — feat/tranche-remaining (closes #193)