From 0885e351b846d01ab1a7c6efeb65400474a8db69 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Mon, 7 Sep 2026 12:32:57 +0100 Subject: [PATCH 1/6] test(contract): add streaming cliff edge tests --- contracts/sharpy/src/test.rs | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 6312c08..634635d 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -4863,3 +4863,70 @@ mod test_audit_hardening { } } +#[cfg(test)] +mod test_edge_streaming_cliff { + use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, Address, Env}; + 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) + } + + #[test] + fn test_withdraw_one_ledger_before_cliff_yields_zero() { + let (env, client) = setup(); + let r = Address::generate(&env); + let start = env.ledger().timestamp(); + client.create_stream(&501u64, &r, &1000i128, &start, &(start + 1000), &(start + 500)); + env.ledger().set_timestamp(start + 499); + assert_eq!(client.withdraw_vested(&501u64, &r), 0i128); + } + + #[test] + fn test_withdraw_exactly_at_cliff_vests() { + let (env, client) = setup(); + let r = Address::generate(&env); + let start = env.ledger().timestamp(); + client.create_stream(&502u64, &r, &1000i128, &start, &(start + 1000), &(start + 500)); + env.ledger().set_timestamp(start + 500); + assert_eq!(client.withdraw_vested(&502u64, &r), 500i128); + } + + #[test] + fn test_second_withdraw_after_full_vesting_yields_zero() { + let (env, client) = setup(); + let r = Address::generate(&env); + let start = env.ledger().timestamp(); + client.create_stream(&503u64, &r, &1000i128, &start, &(start + 1000), &start); + env.ledger().set_timestamp(start + 1001); + assert_eq!(client.withdraw_vested(&503u64, &r), 1000i128); + assert_eq!(client.withdraw_vested(&503u64, &r), 0i128); + } + + #[test] + fn test_withdraw_after_cancel_yields_zero() { + let (env, client) = setup(); + let r = Address::generate(&env); + let start = env.ledger().timestamp(); + client.create_stream(&505u64, &r, &1000i128, &start, &(start + 1000), &start); + assert_eq!(client.cancel_stream(&505u64, &r), 1000i128); + assert_eq!(client.withdraw_vested(&505u64, &r), 0i128); + } + + #[test] + #[should_panic(expected = "end_at must be after start_at")] + fn test_zero_duration_stream_panics() { + let (env, client) = setup(); + let r = Address::generate(&env); + let start = env.ledger().timestamp(); + client.create_stream(&504u64, &r, &1000i128, &start, &start, &start); + } +} + From 882c7723c3cb89557619bf6a2e75c834f356a8f7 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Mon, 7 Sep 2026 12:56:55 +0100 Subject: [PATCH 2/6] test(contract): add routing edge tests --- contracts/sharpy/src/test.rs | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 634635d..e2ee9a1 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -4930,3 +4930,78 @@ mod test_edge_streaming_cliff { } } +#[cfg(test)] +mod test_edge_routing { + 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 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 r = Address::generate(env); + let tok = Address::generate(env); + let dl = env.ledger().timestamp() + 86400; + client.create_invoice( + creator, + &Vec::from_array(env, [r]), + &Vec::from_array(env, [100i128]), + &Vec::from_array(env, [tok]), + &dl, + &opts, + ) + } + + #[test] + fn test_route_overwrite_latest_wins() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id1 = mk(&env, &client, &creator); + let id2 = mk(&env, &client, &creator); + let id3 = mk(&env, &client, &creator); + client.set_route(&creator, &id1, &id2); + assert_eq!(client.resolve_route(&id1), id2); + client.set_route(&creator, &id1, &id3); + assert_eq!(client.get_route(&id1).unwrap().target_invoice, id3); + assert_eq!(client.resolve_route(&id1), id3); + } + + #[test] + #[should_panic(expected = "invoice not found")] + fn test_route_to_missing_invoice_panics() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id1 = mk(&env, &client, &creator); + client.set_route(&creator, &id1, &999_999u64); + } + + #[test] + fn test_resolve_follows_single_hop_only() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id1 = mk(&env, &client, &creator); + let id2 = mk(&env, &client, &creator); + let id3 = mk(&env, &client, &creator); + client.set_route(&creator, &id1, &id2); + client.set_route(&creator, &id2, &id3); + assert_eq!(client.resolve_route(&id1), id2); + assert_eq!(client.resolve_route(&id2), id3); + assert_eq!(client.resolve_route(&id3), id3); + } +} + From 01a39d86ee3b49d937eaf9c5803343d5fa3417b7 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Mon, 7 Sep 2026 12:57:40 +0100 Subject: [PATCH 3/6] test(contract): add tranche cap and auth edge tests --- contracts/sharpy/src/test.rs | 86 ++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index e2ee9a1..e77b629 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -5005,3 +5005,89 @@ mod test_edge_routing { } } + +#[cfg(test)] +mod test_edge_tranche { + 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 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 r = Address::generate(env); + let tok = Address::generate(env); + let dl = env.ledger().timestamp() + 86400; + client.create_invoice( + creator, + &Vec::from_array(env, [r]), + &Vec::from_array(env, [100i128]), + &Vec::from_array(env, [tok]), + &dl, + &opts, + ) + } + + #[test] + fn test_full_release_10000bps_single_call() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id = mk(&env, &client, &creator); + assert_eq!(client.release_tranche(&creator, &id, &10_000u32), 10_000u32); + assert_eq!(client.get_released_bps(&id), 10_000u32); + } + + #[test] + fn test_cumulative_release_sums_to_cap() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id = mk(&env, &client, &creator); + assert_eq!(client.release_tranche(&creator, &id, &4_000u32), 4_000u32); + assert_eq!(client.release_tranche(&creator, &id, &6_000u32), 10_000u32); + assert_eq!(client.get_released_bps(&id), 10_000u32); + } + + #[test] + #[should_panic(expected = "tranches exceed 100%")] + fn test_over_cap_second_release_panics() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.release_tranche(&creator, &id, &6_000u32); + client.release_tranche(&creator, &id, &4_001u32); + } + + #[test] + #[should_panic(expected = "bps out of range")] + fn test_zero_bps_panics() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.release_tranche(&creator, &id, &0u32); + } + + #[test] + #[should_panic(expected = "only creator can release tranches")] + fn test_non_creator_release_panics() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let other = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.release_tranche(&other, &id, &1_000u32); + } +} From 4446f86220e44fbf028464268566578717204516 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Mon, 7 Sep 2026 12:58:10 +0100 Subject: [PATCH 4/6] test(contract): add whitelist open-list edge tests --- contracts/sharpy/src/test.rs | 91 ++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index e77b629..6c23aae 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -5091,3 +5091,94 @@ mod test_edge_tranche { client.release_tranche(&other, &id, &1_000u32); } } + +#[cfg(test)] +mod test_edge_whitelist { + 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 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 r = Address::generate(env); + let tok = Address::generate(env); + let dl = env.ledger().timestamp() + 86400; + client.create_invoice( + creator, + &Vec::from_array(env, [r]), + &Vec::from_array(env, [100i128]), + &Vec::from_array(env, [tok]), + &dl, + &opts, + ) + } + + #[test] + fn test_empty_whitelist_is_open() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let id = mk(&env, &client, &creator); + assert!(client.get_whitelist(&id).is_none()); + client.set_whitelist(&creator, &id, &Vec::new(&env)); + let state = client.get_whitelist(&id).unwrap(); + assert_eq!(state.payers.len(), 0); + } + + #[test] + fn test_add_payer_is_idempotent() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let payer = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.add_whitelisted_payer(&creator, &id, &payer); + client.add_whitelisted_payer(&creator, &id, &payer); + assert_eq!(client.get_whitelist(&id).unwrap().payers.len(), 1); + } + + #[test] + fn test_remove_last_payer_leaves_empty_list() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let payer = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.add_whitelisted_payer(&creator, &id, &payer); + client.remove_whitelisted_payer(&creator, &id, &payer); + assert_eq!(client.get_whitelist(&id).unwrap().payers.len(), 0); + } + + #[test] + #[should_panic(expected = "no whitelist")] + fn test_remove_without_whitelist_panics() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let payer = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.remove_whitelisted_payer(&creator, &id, &payer); + } + + #[test] + #[should_panic(expected = "only creator can set whitelist")] + fn test_non_creator_set_panics() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let other = Address::generate(&env); + let id = mk(&env, &client, &creator); + client.set_whitelist(&other, &id, &Vec::new(&env)); + } +} From 969b79923af8f2a6114a6cfb7d754ee5c6b7764e Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Mon, 7 Sep 2026 12:58:46 +0100 Subject: [PATCH 5/6] test(contract): add fee preview boundary tests --- contracts/sharpy/src/test.rs | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 6c23aae..12f919c 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -5182,3 +5182,61 @@ mod test_edge_whitelist { client.set_whitelist(&other, &id, &Vec::new(&env)); } } + +#[cfg(test)] +mod test_edge_fee { + use soroban_sdk::{testutils::Address as _, Address, Env, Vec}; + use crate::SharpyContractClient; + + fn setup() -> (Env, SharpyContractClient<'static>, Address) { + 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, a) + } + + #[test] + fn test_preview_zero_when_no_fee_configured() { + let (env, client, _) = setup(); + let _ = &env; + assert_eq!(client.preview_fee(&1_000_000i128), 0i128); + } + + #[test] + fn test_preview_zero_amount_is_zero() { + let (env, client, admin) = setup(); + let collector = Address::generate(&env); + client.set_protocol_fee(&500u32, &collector); + let _ = &admin; + assert_eq!(client.preview_fee(&0i128), 0i128); + } + + #[test] + fn test_full_10000bps_fee_equals_amount() { + let (env, client, _) = setup(); + let collector = Address::generate(&env); + client.set_protocol_fee(&10_000u32, &collector); + assert_eq!(client.preview_fee(&12_345i128), 12_345i128); + } + + #[test] + fn test_fractional_fee_truncates_down() { + let (env, client, _) = setup(); + let collector = Address::generate(&env); + client.set_protocol_fee(&100u32, &collector); + assert_eq!(client.preview_fee(&1i128), 0i128); + assert_eq!(client.preview_fee(&10_000i128), 100i128); + } + + #[test] + #[should_panic(expected = "fee bps out of range")] + fn test_fee_over_cap_panics() { + let (env, client, _) = setup(); + let collector = Address::generate(&env); + client.set_protocol_fee(&10_001u32, &collector); + } +} From c225c4a811dbad01bd64c14685c370f87dfd6027 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Mon, 7 Sep 2026 13:00:04 +0100 Subject: [PATCH 6/6] docs(changelog): record new-module edge tests for #182 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3647b..7603bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the Sharpy smart contract are documented here. - feat(contract): `get_creator_invoices_paged(creator, limit, offset)` + `get_payer_invoices_paged(payer, limit, offset)` with total bounds guards (limit 0 / offset past end yield empty pages); unpaginated index queries unchanged — feat/index-pagination (closes #184) - feat(contract): `preview_fee_for_invoice(id)` silent fee estimate on the invoice total (no state change, no events; release math untouched) + `get_ttl_hint(id)` seconds-until-deadline observability view (0 when expired/terminal) — feat/fee-ttl-helpers (closes #185) - feat(contract): audit hardening with zero behavior change — checked `checked_mul/div/add/sub` across fee math, pay/pool_pay/tip funding, streaming vesting, tranche and release accounting; CEI ordering notes on `_release`/`refund`; auth-coverage review documenting permissionless entries and unchanged stream/route auth — feat/audit-hardening (closes #181) +- test(contract): boundary coverage for the five 0.3.0 modules — streaming cliff edges, route overwrite/missing/single-hop, tranche 10000bps cap and creator auth, whitelist open-list/add-idempotent/remove-empty, fee preview zero/full/truncation bounds — test/new-module-edges (closes #182) ## [0.3.0] - 2026-09-04 - feat: streaming payments — `create_stream`/`withdraw_vested`/`cancel_stream`/`top_up_stream` cliff-gated linear vesting (`StreamingState`, 170 tests)