From 64157211712cc1cba05df116574ce4159e96640a Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:12:26 +0100 Subject: [PATCH 1/6] feat(contract): add is_invoice_terminal view --- contracts/sharpy/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs index aa2206f..770b2fa 100644 --- a/contracts/sharpy/src/lib.rs +++ b/contracts/sharpy/src/lib.rs @@ -1179,6 +1179,14 @@ impl SharpyContract { count } + /// True when the invoice is terminal (Released/Refunded/Cancelled). + /// Pure view: `get_ttl_hint == 0` on terminal invoices is expected, not expiry. + /// Use with `is_invoice_expired` to distinguish expired-pending (refundable) + /// from terminal (nothing to do). + pub fn is_invoice_terminal(env: Env, invoice_id: u64) -> bool { + load_invoice(&env, invoice_id).status != InvoiceStatus::Pending + } + /// Read-only deadline-expiry check mirroring the `refund`/`refund_batch` trigger. /// Returns true only when the invoice is still Pending and `timestamp > deadline`. /// Pure view — emits no events. Indexers and UIs should poll this to decide From aa07f84513c59602dbcd5e33d8a0e191da9873d7 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:12:26 +0100 Subject: [PATCH 2/6] docs(contract): clarify ttl-hint zero meanings --- contracts/sharpy/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs index 770b2fa..ed4bdd4 100644 --- a/contracts/sharpy/src/lib.rs +++ b/contracts/sharpy/src/lib.rs @@ -749,6 +749,8 @@ impl SharpyContract { } /// Seconds until `deadline` for a Pending invoice — 0 when expired or terminal. + /// Terminal zeros pair with `is_invoice_terminal`; expired-pending zeros pair + /// with `is_invoice_expired == true` (refund submittable). /// Pure observability view (no state change, no events) for long-lived invoice /// dashboards: poll alongside `is_invoice_expired` and submit `bump_invoice_ttl` /// (storage keep-alive) or `extend_deadline` (push the deadline out) while the From ba5395738bebeb3faa5835075f8f30601a682e25 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:12:32 +0100 Subject: [PATCH 3/6] test(contract): add ttl countdown and expired-pending cases --- contracts/sharpy/src/test.rs | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 4d99d49..4b506ba 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -6023,3 +6023,46 @@ mod test_paged_total_c { assert_eq!(page.len(), 1u32); } } + +#[cfg(test)] +mod test_ttl_terminal_a { + use soroban_sdk::{testutils::Address as _, testutils::Ledger 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, dl: u64) -> u64 { + let r = Address::generate(env); + let tok = Address::generate(env); + 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_countdown_exact() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let now = env.ledger().timestamp(); + let id = mk(&env, &client, &creator, now + 1000); + assert_eq!(client.get_ttl_hint(&id), 1000u64); + assert!(!client.is_invoice_terminal(&id)); + assert!(!client.is_invoice_expired(&id)); + } + #[test] + fn test_expired_pending_hint_zero_and_refundable() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let now = env.ledger().timestamp(); + let id = mk(&env, &client, &creator, now + 10); + env.ledger().set_timestamp(now + 11); + assert_eq!(client.get_ttl_hint(&id), 0u64); + assert!(client.is_invoice_expired(&id)); + assert!(!client.is_invoice_terminal(&id)); + } +} From e5e828884f8ad6f708e1716bceda98369a0e3cfe Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:12:32 +0100 Subject: [PATCH 4/6] test(contract): add ttl released and cancelled terminal cases --- contracts/sharpy/src/test.rs | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index 4b506ba..d4f0d88 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -6066,3 +6066,50 @@ mod test_ttl_terminal_a { assert!(!client.is_invoice_terminal(&id)); } } + +#[cfg(test)] +mod test_ttl_terminal_b { + use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, token, Address, Env, Vec}; + use crate::{types::InvoiceStatus, 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_released_terminal_hint_zero() { + 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, &5000i128); + 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 r = Address::generate(&env); + let id = client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [1000i128]), &Vec::from_array(&env, [tok]), &dl, &opts); + client.pay(&payer, &id, &1000i128); + assert_eq!(client.get_invoice(&id).status, InvoiceStatus::Released); + assert_eq!(client.get_ttl_hint(&id), 0u64); + assert!(client.is_invoice_terminal(&id)); + assert!(!client.is_invoice_expired(&id)); + } + #[test] + fn test_cancelled_terminal_hint_zero() { + 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); + client.cancel_invoice(&creator, &id); + assert_eq!(client.get_ttl_hint(&id), 0u64); + assert!(client.is_invoice_terminal(&id)); + } +} From f7dc54609d16ecf79207e5ce1bee0b42f922cf82 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:12:37 +0100 Subject: [PATCH 5/6] test(contract): add ttl refunded terminal and extend-revive cases --- contracts/sharpy/src/test.rs | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs index d4f0d88..e00436c 100644 --- a/contracts/sharpy/src/test.rs +++ b/contracts/sharpy/src/test.rs @@ -6113,3 +6113,51 @@ mod test_ttl_terminal_b { assert!(client.is_invoice_terminal(&id)); } } + +#[cfg(test)] +mod test_ttl_terminal_c { + use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, token, Address, Env, Vec}; + use crate::{types::InvoiceStatus, 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_refunded_terminal_hint_zero() { + 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, &2000i128); + let now = env.ledger().timestamp(); + 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 id = client.create_invoice(&creator, &Vec::from_array(&env, [r]), &Vec::from_array(&env, [1000i128]), &Vec::from_array(&env, [tok]), &(now + 10), &opts); + client.pay(&payer, &id, &400i128); + env.ledger().set_timestamp(now + 11); + client.refund(&id); + assert_eq!(client.get_invoice(&id).status, InvoiceStatus::Refunded); + assert_eq!(client.get_ttl_hint(&id), 0u64); + assert!(client.is_invoice_terminal(&id)); + } + #[test] + fn test_extend_revives_hint() { + let (env, client) = setup(); + let creator = Address::generate(&env); + let r = Address::generate(&env); + let tok = Address::generate(&env); + let now = env.ledger().timestamp(); + 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]), &(now + 100), &opts); + client.extend_deadline(&creator, &id, &(now + 1000)); + assert_eq!(client.get_ttl_hint(&id), 1000u64); + assert!(!client.is_invoice_expired(&id)); + } +} From 36a45effaed33f8c956be4ca987dea829c17ff35 Mon Sep 17 00:00:00 2001 From: MarcusDavidG Date: Thu, 10 Sep 2026 12:12:58 +0100 Subject: [PATCH 6/6] docs(changelog): record ttl terminal coverage for #197 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614fbfa..19ac4d0 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): `is_invoice_terminal(id)` view + ttl-hint terminal/expired-mirror coverage (countdown, Released/Refunded/Cancelled zeros, extend revives) — feat/ttl-terminal (closes #197) - 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)