Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions contracts/sharpy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -846,6 +847,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<u64>>(&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<u64>>(&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.
Expand Down
140 changes: 140 additions & 0 deletions contracts/sharpy/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5883,3 +5883,143 @@ 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);
}
}

#[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);
}
}

#[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);
}
}
Loading