Skip to content

Commit 1ea959a

Browse files
authored
Merge pull request #57 from marvs8/feature/33-invoice-group-release
feat: add invoice group all-or-nothing release
2 parents aec6a66 + 3f8ac9f commit 1ea959a

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

contracts/split/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,35 @@ fn require_admin(env: &Env, caller: &Address) {
9090
caller.require_auth();
9191
}
9292

93+
/// Composite storage key for a group: (symbol, group_id).
94+
fn group_key(group_id: u64) -> (Symbol, u64) {
95+
(symbol_short!("grp"), group_id)
96+
}
97+
98+
fn load_group(env: &Env, group_id: u64) -> Vec<u64> {
99+
env.storage()
100+
.persistent()
101+
.get(&group_key(group_id))
102+
.expect("group not found")
103+
}
104+
105+
/// Storage key mapping an invoice ID to its group ID.
106+
fn invoice_group_key(invoice_id: u64) -> (Symbol, u64) {
107+
(symbol_short!("invgrp"), invoice_id)
108+
}
109+
110+
/// Returns true only if every invoice in the group is fully funded.
111+
fn group_all_funded(env: &Env, group_id: u64) -> bool {
112+
for id in load_group(env, group_id).iter() {
113+
let inv = load_invoice(env, id);
114+
let total: i128 = inv.amounts.iter().sum();
115+
if inv.funded < total {
116+
return false;
117+
}
118+
}
119+
true
120+
}
121+
93122
// ---------------------------------------------------------------------------
94123
// Contract
95124
// ---------------------------------------------------------------------------

contracts/split/src/test.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,118 @@ fn test_bonus_pool_zero_behaves_identically() {
652652
assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released);
653653
assert_eq!(tk.balance(&recipient), 200);
654654
}
655+
656+
#[test]
657+
#[should_panic(expected = "group members not fully funded")]
658+
fn test_group_partial_fund_blocks_release() {
659+
let (env, contract_id, token_id) = setup();
660+
let c = client(&env, &contract_id);
661+
662+
let creator = Address::generate(&env);
663+
let payer = Address::generate(&env);
664+
let r1 = Address::generate(&env);
665+
let r2 = Address::generate(&env);
666+
667+
let stellar_asset = StellarAssetClient::new(&env, &token_id);
668+
stellar_asset.mint(&payer, &1_000);
669+
670+
env.ledger().set_timestamp(1_000);
671+
672+
let mut rec1 = Vec::new(&env);
673+
rec1.push_back(r1.clone());
674+
let mut amt1 = Vec::new(&env);
675+
amt1.push_back(100_i128);
676+
677+
let mut rec2 = Vec::new(&env);
678+
rec2.push_back(r2.clone());
679+
let mut amt2 = Vec::new(&env);
680+
amt2.push_back(200_i128);
681+
682+
let id1 = c.create_invoice(&creator, &rec1, &amt1, &token_id, &9_999_u64);
683+
let id2 = c.create_invoice(&creator, &rec2, &amt2, &token_id, &9_999_u64);
684+
685+
let mut ids = Vec::new(&env);
686+
ids.push_back(id1);
687+
ids.push_back(id2);
688+
c.create_invoice_group(&ids);
689+
690+
// Fund only invoice 1 fully.
691+
c.pay(&payer, &id1, &100_i128);
692+
693+
// Attempt to release invoice 1 — should panic because invoice 2 is not funded.
694+
c.release(&id1);
695+
}
696+
697+
#[test]
698+
fn test_group_all_funded_releases_both() {
699+
let (env, contract_id, token_id) = setup();
700+
let c = client(&env, &contract_id);
701+
let tk = token_client(&env, &token_id);
702+
703+
let creator = Address::generate(&env);
704+
let payer = Address::generate(&env);
705+
let r1 = Address::generate(&env);
706+
let r2 = Address::generate(&env);
707+
708+
let stellar_asset = StellarAssetClient::new(&env, &token_id);
709+
stellar_asset.mint(&payer, &1_000);
710+
711+
env.ledger().set_timestamp(1_000);
712+
713+
let mut rec1 = Vec::new(&env);
714+
rec1.push_back(r1.clone());
715+
let mut amt1 = Vec::new(&env);
716+
amt1.push_back(100_i128);
717+
718+
let mut rec2 = Vec::new(&env);
719+
rec2.push_back(r2.clone());
720+
let mut amt2 = Vec::new(&env);
721+
amt2.push_back(200_i128);
722+
723+
let id1 = c.create_invoice(&creator, &rec1, &amt1, &token_id, &9_999_u64);
724+
let id2 = c.create_invoice(&creator, &rec2, &amt2, &token_id, &9_999_u64);
725+
726+
let mut ids = Vec::new(&env);
727+
ids.push_back(id1);
728+
ids.push_back(id2);
729+
c.create_invoice_group(&ids);
730+
731+
// Fund both invoices fully.
732+
c.pay(&payer, &id1, &100_i128);
733+
c.pay(&payer, &id2, &200_i128);
734+
735+
// Release via either member — both should be released.
736+
c.release(&id1);
737+
738+
assert_eq!(c.get_invoice(&id1).status, InvoiceStatus::Released);
739+
assert_eq!(c.get_invoice(&id2).status, InvoiceStatus::Released);
740+
assert_eq!(tk.balance(&r1), 100);
741+
assert_eq!(tk.balance(&r2), 200);
742+
}
743+
744+
#[test]
745+
fn test_non_grouped_invoice_unaffected() {
746+
let (env, contract_id, token_id) = setup();
747+
let c = client(&env, &contract_id);
748+
let tk = token_client(&env, &token_id);
749+
750+
let creator = Address::generate(&env);
751+
let payer = Address::generate(&env);
752+
let recipient = Address::generate(&env);
753+
754+
let stellar_asset = StellarAssetClient::new(&env, &token_id);
755+
stellar_asset.mint(&payer, &300);
756+
757+
env.ledger().set_timestamp(1_000);
758+
759+
let mut recipients = Vec::new(&env);
760+
recipients.push_back(recipient.clone());
761+
let mut amounts = Vec::new(&env);
762+
amounts.push_back(300_i128);
763+
764+
let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64);
765+
c.pay(&payer, &id, &300_i128);
766+
767+
assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released);
768+
assert_eq!(tk.balance(&recipient), 300);
769+
}

0 commit comments

Comments
 (0)