Skip to content
Open
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
26 changes: 20 additions & 6 deletions contracts/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ fn load_invoice(env: &Env, id: u64) -> Invoice {
auction_end: 0,
bids: Vec::new(env),
min_payment: 0,
min_funding_amount: 0,
});
Invoice::assemble(core, ext, ext2)
}
Expand Down Expand Up @@ -696,6 +697,7 @@ impl SplitContract {
options.auto_resolve_rules,
options.cross_chain_ref,
options.allowed_payers,
options.min_funding_amount.unwrap_or(0),
)
}

Expand Down Expand Up @@ -739,6 +741,7 @@ impl SplitContract {
auto_resolve_rules: Vec<ResolveRule>,
cross_chain_ref: Option<String>,
allowed_payers: Option<Vec<Address>>,
min_funding_amount: i128,
) -> u64 {
assert!(
recipients.len() == amounts.len(),
Expand Down Expand Up @@ -962,6 +965,7 @@ impl SplitContract {
auction_end: 0,
bids: Vec::new(env),
min_payment: 0,
min_funding_amount,
};

save_invoice(env, id, &invoice);
Expand Down Expand Up @@ -1086,6 +1090,7 @@ impl SplitContract {
Vec::new(&env),
None,
None,
0,
);
ids.push_back(id);
}
Expand Down Expand Up @@ -1153,6 +1158,7 @@ impl SplitContract {
Vec::new(&env),
None,
None,
0,
);

if months > 1 {
Expand Down Expand Up @@ -1285,7 +1291,8 @@ impl SplitContract {
|| !invoice.release_stages.is_empty()
|| in_group
|| !invoice.co_signers.is_empty()
|| (invoice.oracle_address.is_some() && !invoice.condition_met);
|| (invoice.oracle_address.is_some() && !invoice.condition_met)
|| (invoice.min_funding_amount > 0 && invoice.funded < invoice.min_funding_amount);
if guarded {
save_invoice(&env, invoice_id, &invoice);
} else {
Expand Down Expand Up @@ -1534,7 +1541,8 @@ impl SplitContract {
|| !invoice.release_stages.is_empty()
|| in_group
|| !invoice.co_signers.is_empty()
|| (invoice.oracle_address.is_some() && !invoice.condition_met);
|| (invoice.oracle_address.is_some() && !invoice.condition_met)
|| (invoice.min_funding_amount > 0 && invoice.funded < invoice.min_funding_amount);
if guarded {
save_invoice(env, invoice_id, &invoice);
} else {
Expand Down Expand Up @@ -1633,7 +1641,8 @@ impl SplitContract {
|| !invoice.tranches.is_empty()
|| !invoice.release_stages.is_empty()
|| in_group
|| !invoice.co_signers.is_empty();
|| !invoice.co_signers.is_empty()
|| (invoice.min_funding_amount > 0 && invoice.funded < invoice.min_funding_amount);
if guarded {
save_invoice(&env, invoice_id, &invoice);
} else {
Expand Down Expand Up @@ -1695,7 +1704,8 @@ impl SplitContract {
|| !invoice.release_stages.is_empty()
|| in_group
|| !invoice.co_signers.is_empty()
|| (invoice.oracle_address.is_some() && !invoice.condition_met);
|| (invoice.oracle_address.is_some() && !invoice.condition_met)
|| (invoice.min_funding_amount > 0 && invoice.funded < invoice.min_funding_amount);
if guarded {
save_invoice(&env, invoice_id, &invoice);
} else {
Expand Down Expand Up @@ -1771,7 +1781,8 @@ impl SplitContract {
|| !inv.release_stages.is_empty()
|| in_group
|| !inv.co_signers.is_empty()
|| (inv.oracle_address.is_some() && !inv.condition_met);
|| (inv.oracle_address.is_some() && !inv.condition_met)
|| (inv.min_funding_amount > 0 && inv.funded < inv.min_funding_amount);
if guarded {
save_invoice(&env, p.invoice_id, &inv);
} else {
Expand Down Expand Up @@ -2732,6 +2743,7 @@ impl SplitContract {
Vec::new(env),
None,
None,
0,
);
env.storage()
.persistent()
Expand Down Expand Up @@ -3260,6 +3272,7 @@ impl SplitContract {
old_invoice.auto_resolve_rules.clone(),
old_invoice.cross_chain_ref.clone(),
None,
old_invoice.min_funding_amount,
);

// Load the newly created invoice and copy over the payments.
Expand Down Expand Up @@ -3472,6 +3485,7 @@ impl SplitContract {
Vec::new(&env),
None,
None,
0,
)
}

Expand Down Expand Up @@ -3900,7 +3914,7 @@ impl SplitContract {
.unwrap_or_else(|| InvoiceExt2 {
notification_contract: None, overflow_behavior: OverflowBehavior::Reject,
cross_chain_ref: None, require_kyc: false, auction_on_expiry: false,
auction_end: 0, bids: Vec::new(&env), min_payment: 0,
auction_end: 0, bids: Vec::new(&env), min_payment: 0, min_funding_amount: 0,
});

// Copy to instance storage.
Expand Down
59 changes: 59 additions & 0 deletions contracts/split/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ fn default_options(env: &Env) -> InvoiceOptions {
oracle_address: None,
cross_chain_ref: None,
allowed_payers: None,
min_funding_amount: None,
}
}

Expand Down Expand Up @@ -4118,3 +4119,61 @@ fn test_creator_stats_increments_on_operations() {
assert_eq!(released, 2);
assert_eq!(refunded, 1);
}

#[test]
fn test_min_funding_amount_delays_release() {
let (env, contract_id, token_id) = setup();
let c = client(&env, &contract_id);

let creator = Address::generate(&env);
let payer = Address::generate(&env);
let recipient = Address::generate(&env);

StellarAssetClient::new(&env, &token_id).mint(&payer, &500);
env.ledger().set_timestamp(1_000);

// Total = 100 but min_funding_amount = 200, so fully paying 100 should not auto-release.
let mut opts = default_options(&env);
opts.min_funding_amount = Some(200);

let mut recipients = Vec::new(&env);
recipients.push_back(recipient.clone());
let mut amounts = Vec::new(&env);
amounts.push_back(100_i128);
let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);

c.pay(&payer, &id, &100_i128, &0_u64, &false);

let invoice = c.get_invoice(&id);
assert_eq!(invoice.funded, 100);
assert_eq!(invoice.status, InvoiceStatus::Pending);
}

#[test]
fn test_min_funding_amount_releases_when_met() {
let (env, contract_id, token_id) = setup();
let c = client(&env, &contract_id);

let creator = Address::generate(&env);
let payer = Address::generate(&env);
let recipient = Address::generate(&env);

StellarAssetClient::new(&env, &token_id).mint(&payer, &500);
env.ledger().set_timestamp(1_000);

// Total = 100, min_funding_amount = 50 — fully paying 100 meets both thresholds.
let mut opts = default_options(&env);
opts.min_funding_amount = Some(50);

let mut recipients = Vec::new(&env);
recipients.push_back(recipient.clone());
let mut amounts = Vec::new(&env);
amounts.push_back(100_i128);
let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);

c.pay(&payer, &id, &100_i128, &0_u64, &false);

let invoice = c.get_invoice(&id);
assert_eq!(invoice.funded, 100);
assert_eq!(invoice.status, InvoiceStatus::Released);
}
7 changes: 7 additions & 0 deletions contracts/split/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ pub struct InvoiceOptions {
pub cross_chain_ref: Option<String>,
/// Issue #98: restrict payments to this allowlist; None = open.
pub allowed_payers: Option<Vec<Address>>,
/// Absolute minimum funded amount required before auto-release triggers.
pub min_funding_amount: Option<i128>,
}

/// Legacy invoice layout used by stored invoices created before the `version`
Expand Down Expand Up @@ -312,6 +314,7 @@ pub struct InvoiceExt2 {
pub auction_end: u64,
pub bids: Vec<Bid>,
pub min_payment: i128,
pub min_funding_amount: i128,
}

/// Full invoice — assembled from InvoiceCore + InvoiceExt + InvoiceExt2.
Expand Down Expand Up @@ -377,6 +380,7 @@ pub struct Invoice {
pub auction_end: u64,
pub bids: Vec<Bid>,
pub min_payment: i128,
pub min_funding_amount: i128,
}

impl Invoice {
Expand Down Expand Up @@ -446,6 +450,7 @@ impl Invoice {
auction_end: self.auction_end,
bids: self.bids,
min_payment: self.min_payment,
min_funding_amount: self.min_funding_amount,
},
)
}
Expand Down Expand Up @@ -511,6 +516,7 @@ impl Invoice {
auction_end: ext2.auction_end,
bids: ext2.bids,
min_payment: ext2.min_payment,
min_funding_amount: ext2.min_funding_amount,
}
}
}
Expand Down Expand Up @@ -597,6 +603,7 @@ impl Invoice {
notification_contract: None,
overflow_behavior: OverflowBehavior::Reject,
cross_chain_ref: None,
min_funding_amount: 0,
}
}
}