Skip to content

Commit b5331a0

Browse files
Add split_to_escrow tests, contract_paused_for, get_all_escrows_between, and cancel_recurring_batch tests
- Add contract_paused_for view returning ledgers elapsed since the contract was paused (#739) - Add get_all_escrows_between view returning all escrow IDs between a depositor and beneficiary (#738) - Add split_to_escrow test coverage (#746) - Add cancel_recurring_batch test coverage (#737) - Repair pre-existing upstream merge damage (duplicate VeritixContract blocks, mid-file use statements) so the crate compiles
1 parent 04cd94f commit b5331a0

8 files changed

Lines changed: 384 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ for contract upgrades.
1616

1717
### Added
1818

19+
- `contract_paused_for` view returning how many ledgers the contract has been paused (#739).
20+
- `get_all_escrows_between` view returning all escrow IDs (active and settled) between a depositor and beneficiary (#738).
21+
- Tests for `split_to_escrow` (#746) and `cancel_recurring_batch` (#737).
1922
- Pre-commit hook (`make install-hooks`) running `cargo fmt` and `cargo clippy` before every commit.
2023
- CHANGELOG.md tracking all significant changes per release.
2124
- Inline doc comments explaining the purpose of each test scenario across all test files.

src/contract.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ pub trait VeriTixPayTrait {
675675
/// # Arguments
676676
/// - `e` — contract environment (auto-injected).
677677
fn is_paused(e: Env) -> bool;
678+
fn contract_paused_for(e: Env) -> Option<u32>;
678679

679680
// ── Permit / Nonce ────────────────────────────────────────────────────────
680681
/// Consumes the current nonce for `user`, replay-protecting subsequent
@@ -998,6 +999,8 @@ pub trait VeriTixPayTrait {
998999
/// - `no escrow found between the two addresses` if no shared escrow exists.
9991000
fn escrow_between(e: Env, addr1: Address, addr2: Address) -> u32;
10001001

1002+
fn get_all_escrows_between(e: Env, depositor: Address, beneficiary: Address) -> Vec<u32>;
1003+
10011004
/// Cancels up to 20 recurring payments in one call. The payer authenticates.
10021005
///
10031006
/// # Arguments
@@ -1715,6 +1718,10 @@ impl VeriTixPayTrait for VeriTixPay {
17151718
.unwrap_or(false)
17161719
}
17171720

1721+
fn contract_paused_for(e: Env) -> Option<u32> {
1722+
crate::pause::contract_paused_for(&e)
1723+
}
1724+
17181725
// ── Permit / Nonce ────────────────────────────────────────────────────────
17191726

17201727
fn permit(e: Env, user: Address, nonce: u32) {
@@ -1802,6 +1809,10 @@ impl VeriTixPayTrait for VeriTixPay {
18021809
escrow::escrow_between(e, addr1, addr2)
18031810
}
18041811

1812+
fn get_all_escrows_between(e: Env, depositor: Address, beneficiary: Address) -> Vec<u32> {
1813+
escrow::get_all_escrows_between(e, depositor, beneficiary)
1814+
}
1815+
18051816
fn cancel_recurring_batch(e: Env, caller: Address, recurring_ids: Vec<u32>) {
18061817
recurring::cancel_recurring_batch(&e, &caller, recurring_ids)
18071818
}
@@ -2345,10 +2356,7 @@ impl VeritixContract {
23452356
/// A vector of [`RecurringExecution`] entries (empty if none recorded).
23462357
pub fn get_recurring_history(e: Env, recurring_id: u32) -> Vec<RecurringExecution> {
23472358
let key = DataKey::RecurringHistory(recurring_id);
2348-
e.storage()
2349-
.instance()
2350-
.get(&key)
2351-
.unwrap_or_else(|| Vec::new(&e))
2359+
e.storage().instance().get(&key).unwrap_or_else(|| Vec::new(&e))
23522360
}
23532361

23542362
/// Retrieves all recurring payment IDs associated with a specific payee address.
@@ -2361,10 +2369,7 @@ impl VeritixContract {
23612369
/// A vector of recurring payment IDs (empty if none exist).
23622370
pub fn get_recurring_by_payee(e: Env, payee: Address) -> Vec<u32> {
23632371
let key = DataKey::PayeeRecurrings(payee);
2364-
e.storage()
2365-
.instance()
2366-
.get(&key)
2367-
.unwrap_or_else(|| Vec::new(&e))
2372+
e.storage().instance().get(&key).unwrap_or_else(|| Vec::new(&e))
23682373
}
23692374

23702375
/// Returns a boolean indicating whether a recurring payment schedule is currently active and not paused,

src/escrow.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,25 @@ pub fn get_escrows_by_depositor(e: Env, depositor: Address) -> Vec<u32> {
563563
read_escrow_ids(&e, DataKey::DepositorEscrows(depositor))
564564
}
565565

566+
// #738: Return all escrow IDs (active and settled) between a depositor and
567+
// beneficiary, in creation order, for a full audit trail.
568+
pub fn get_all_escrows_between(e: Env, depositor: Address, beneficiary: Address) -> Vec<u32> {
569+
let escrows = get_escrows_by_depositor(e.clone(), depositor);
570+
let mut result = Vec::new(&e);
571+
for i in 0..escrows.len() {
572+
let id = escrows.get(i).unwrap();
573+
let record: EscrowRecord = e
574+
.storage()
575+
.persistent()
576+
.get(&DataKey::Escrow(id))
577+
.unwrap_or_else(|| panic!("escrow {} not found", id));
578+
if record.beneficiary == beneficiary {
579+
result.push_back(id);
580+
}
581+
}
582+
result
583+
}
584+
566585
pub fn get_escrows_by_beneficiary(e: Env, beneficiary: Address) -> Vec<u32> {
567586
read_escrow_ids(&e, DataKey::BeneficiaryEscrows(beneficiary))
568587
}

src/escrow_test.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,3 +1414,67 @@ fn test_protocol_fee_not_applied_to_refunds() {
14141414
assert_eq!(token_client.balance(&t.depositor), 50_000_000);
14151415
assert_eq!(token_client.balance(&t.treasury), 0);
14161416
}
1417+
1418+
// ── #738: get_all_escrows_between ────────────────────────────────────────────
1419+
1420+
#[test]
1421+
fn test_get_all_escrows_between_includes_active_and_settled() {
1422+
let t = setup();
1423+
mint_more(&t, crate::storage_types::MIN_ESCROW_AMOUNT * 3);
1424+
let expiry = t.e.ledger().sequence() + 1000;
1425+
1426+
let id1 = t.client.create_escrow(
1427+
&t.depositor,
1428+
&t.beneficiary,
1429+
&t.token,
1430+
&crate::storage_types::MIN_ESCROW_AMOUNT,
1431+
&expiry,
1432+
&empty_memo(&t.e),
1433+
);
1434+
let id2 = t.client.create_escrow(
1435+
&t.depositor,
1436+
&t.beneficiary,
1437+
&t.token,
1438+
&crate::storage_types::MIN_ESCROW_AMOUNT,
1439+
&expiry,
1440+
&empty_memo(&t.e),
1441+
);
1442+
1443+
// Settle id1 so it is part of the finished history.
1444+
t.client.release_escrow(&t.depositor, &id1);
1445+
assert!(t.client.get_escrow(&id1).released);
1446+
1447+
let ids = t.client.get_all_escrows_between(&t.depositor, &t.beneficiary);
1448+
assert_eq!(ids.len(), 2);
1449+
assert_eq!(ids.get(0).unwrap(), id1);
1450+
assert_eq!(ids.get(1).unwrap(), id2);
1451+
}
1452+
1453+
#[test]
1454+
fn test_get_all_escrows_between_filters_other_beneficiary() {
1455+
let t = setup();
1456+
mint_more(&t, crate::storage_types::MIN_ESCROW_AMOUNT * 3);
1457+
let expiry = t.e.ledger().sequence() + 1000;
1458+
let other = Address::generate(&t.e);
1459+
soroban_sdk::token::StellarAssetClient::new(&t.e, &t.token).mint(&other, &crate::storage_types::MIN_ESCROW_AMOUNT);
1460+
1461+
let _id1 = t.client.create_escrow(
1462+
&t.depositor,
1463+
&t.beneficiary,
1464+
&t.token,
1465+
&crate::storage_types::MIN_ESCROW_AMOUNT,
1466+
&expiry,
1467+
&empty_memo(&t.e),
1468+
);
1469+
1470+
// Escrow between the depositor and `other` does not exist yet.
1471+
let ids = t.client.get_all_escrows_between(&t.depositor, &other);
1472+
assert_eq!(ids.len(), 0);
1473+
}
1474+
1475+
#[test]
1476+
fn test_get_all_escrows_between_empty_with_no_escrows() {
1477+
let t = setup();
1478+
let ids = t.client.get_all_escrows_between(&t.depositor, &t.beneficiary);
1479+
assert_eq!(ids.len(), 0);
1480+
}

src/pause.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,29 @@ pub fn require_not_paused(e: &Env) {
1414
pub fn set_paused(e: &Env, caller: &Address, paused: bool) {
1515
crate::admin::check_admin(e, caller);
1616
e.storage().persistent().set(&DataKey::Paused, &paused);
17+
if paused {
18+
e.storage()
19+
.persistent()
20+
.set(&DataKey::PausedAtLedger, &e.ledger().sequence());
21+
} else {
22+
e.storage().persistent().remove(&DataKey::PausedAtLedger);
23+
}
24+
}
25+
26+
/// #739: Return how many ledgers the contract has been continuously paused for.
27+
pub fn contract_paused_for(e: &Env) -> Option<u32> {
28+
let paused: bool = e
29+
.storage()
30+
.persistent()
31+
.get::<_, bool>(&DataKey::Paused)
32+
.unwrap_or(false);
33+
if !paused {
34+
return None;
35+
}
36+
let paused_at: u32 = e
37+
.storage()
38+
.persistent()
39+
.get(&DataKey::PausedAtLedger)
40+
.expect("paused_at_ledger not set when paused");
41+
Some(e.ledger().sequence().saturating_sub(paused_at))
1742
}

src/pause_test.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![cfg(test)]
22

33
use crate::contract::{VeriTixPay, VeriTixPayClient};
4-
use soroban_sdk::{testutils::Address as _, Address, Env};
4+
use soroban_sdk::{testutils::Address as _, testutils::Ledger as _, Address, Env};
55

66
struct TestEnv<'a> {
77
e: Env,
@@ -104,3 +104,27 @@ fn test_unpause_re_allows_transfer() {
104104
t.client.transfer_from(&spender, &from, &to, &100);
105105
assert_eq!(t.client.balance(&to), 100);
106106
}
107+
108+
// ── #739: contract_paused_for ─────────────────────────────────────────────────
109+
110+
#[test]
111+
fn test_contract_paused_for_returns_none_when_not_paused() {
112+
let t = setup();
113+
assert_eq!(t.client.contract_paused_for(), None);
114+
}
115+
116+
#[test]
117+
fn test_contract_paused_for_returns_ledgers_elapsed_since_pause() {
118+
let t = setup();
119+
t.client.set_paused(&t.admin, &true);
120+
assert!(t.client.contract_paused_for().is_some());
121+
122+
// Advance the ledger and confirm the elapsed count grows.
123+
let before = t.client.contract_paused_for().unwrap();
124+
t.e.ledger().with_mut(|l| l.sequence_number += 5);
125+
assert_eq!(t.client.contract_paused_for().unwrap(), before + 5);
126+
127+
// Unpausing resets the counter to None.
128+
t.client.set_paused(&t.admin, &false);
129+
assert_eq!(t.client.contract_paused_for(), None);
130+
}

src/recurring_test.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,3 +671,100 @@ fn test_execute_recurring_without_window_allows_late_execution() {
671671
client.execute_recurring(&id);
672672
assert!(client.is_recurring_active(&id));
673673
}
674+
675+
// ── #737: cancel_recurring_batch tests ───────────────────────────────────────
676+
677+
#[test]
678+
fn test_cancel_recurring_batch_all_succeed() {
679+
use soroban_sdk::{testutils::Address as _, Address};
680+
let e = Env::default();
681+
e.mock_all_auths();
682+
683+
let contract_id = e.register_contract(None, crate::contract::VeriTixPay);
684+
let client = crate::contract::VeriTixPayClient::new(&e, &contract_id);
685+
686+
let payer = Address::generate(&e);
687+
let payee = Address::generate(&e);
688+
let token = e.register_stellar_asset_contract(Address::generate(&e));
689+
soroban_sdk::token::StellarAssetClient::new(&e, &token).mint(&payer, &10_000);
690+
691+
let id1 = client.setup_recurring(&payer, &payee, &token, &100, &100, &5);
692+
let id2 = client.setup_recurring(&payer, &payee, &token, &100, &100, &5);
693+
assert!(client.is_recurring_active(&id1));
694+
assert!(client.is_recurring_active(&id2));
695+
696+
client.cancel_recurring_batch(&payer, &soroban_sdk::vec![&e, id1, id2]);
697+
assert!(!client.is_recurring_active(&id1));
698+
assert!(!client.is_recurring_active(&id2));
699+
}
700+
701+
#[test]
702+
#[should_panic(expected = "not the payer for recurring")]
703+
fn test_cancel_recurring_batch_one_wrong_payer_reverts_all() {
704+
use soroban_sdk::{testutils::Address as _, Address};
705+
let e = Env::default();
706+
e.mock_all_auths();
707+
708+
let contract_id = e.register_contract(None, crate::contract::VeriTixPay);
709+
let client = crate::contract::VeriTixPayClient::new(&e, &contract_id);
710+
711+
let payer = Address::generate(&e);
712+
let intruder = Address::generate(&e);
713+
let payee = Address::generate(&e);
714+
let token = e.register_stellar_asset_contract(Address::generate(&e));
715+
soroban_sdk::token::StellarAssetClient::new(&e, &token).mint(&payer, &10_000);
716+
soroban_sdk::token::StellarAssetClient::new(&e, &token).mint(&intruder, &10_000);
717+
718+
let id1 = client.setup_recurring(&payer, &payee, &token, &100, &100, &5);
719+
let id2 = client.setup_recurring(&payer, &payee, &token, &100, &100, &5);
720+
721+
// An intruder trying to cancel a batch containing a payer-owned id fails.
722+
client.cancel_recurring_batch(&intruder, &soroban_sdk::vec![&e, id1, id2]);
723+
}
724+
725+
#[test]
726+
#[should_panic(expected = "batch size cannot exceed 20")]
727+
fn test_cancel_recurring_batch_over_limit_panics() {
728+
use soroban_sdk::{testutils::Address as _, Address};
729+
let e = Env::default();
730+
e.mock_all_auths();
731+
732+
let contract_id = e.register_contract(None, crate::contract::VeriTixPay);
733+
let client = crate::contract::VeriTixPayClient::new(&e, &contract_id);
734+
735+
let payer = Address::generate(&e);
736+
let payee = Address::generate(&e);
737+
let token = e.register_stellar_asset_contract(Address::generate(&e));
738+
soroban_sdk::token::StellarAssetClient::new(&e, &token).mint(&payer, &10_000);
739+
740+
let mut ids: soroban_sdk::Vec<u32> = soroban_sdk::Vec::new(&e);
741+
for _ in 0..25 {
742+
ids.push_back(client.setup_recurring(&payer, &payee, &token, &100, &100, &5));
743+
}
744+
client.cancel_recurring_batch(&payer, &ids);
745+
}
746+
747+
#[test]
748+
fn test_cancel_recurring_batch_removes_all_from_payer_index() {
749+
use soroban_sdk::{testutils::Address as _, Address};
750+
let e = Env::default();
751+
e.mock_all_auths();
752+
753+
let contract_id = e.register_contract(None, crate::contract::VeriTixPay);
754+
let client = crate::contract::VeriTixPayClient::new(&e, &contract_id);
755+
756+
let payer = Address::generate(&e);
757+
let payee = Address::generate(&e);
758+
let token = e.register_stellar_asset_contract(Address::generate(&e));
759+
soroban_sdk::token::StellarAssetClient::new(&e, &token).mint(&payer, &10_000);
760+
761+
let id1 = client.setup_recurring(&payer, &payee, &token, &100, &100, &5);
762+
let id2 = client.setup_recurring(&payer, &payee, &token, &100, &100, &5);
763+
let list_before = client.get_recurring_by_payer(&payer);
764+
assert_eq!(list_before.len(), 2);
765+
766+
client.cancel_recurring_batch(&payer, &soroban_sdk::vec![&e, id1, id2]);
767+
// Every recurring the payer owned is now inactive.
768+
assert!(!client.is_recurring_active(&id1));
769+
assert!(!client.is_recurring_active(&id2));
770+
}

0 commit comments

Comments
 (0)