Skip to content

Commit 2ec6b11

Browse files
committed
feat: add multi-token batch payment support to pool_pay
Group payments by token across invoices and perform one transfer per token, enabling batch payment of invoices using different tokens in a single pool_pay call. Also fixes missing escrow release timestamp when an escrow invoice is fully funded via pool_pay. Closes #3
1 parent b6b9d4e commit 2ec6b11

2 files changed

Lines changed: 158 additions & 8 deletions

File tree

contracts/sharpy/src/lib.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,29 +241,41 @@ impl SharpyContract {
241241
payer.require_auth();
242242
assert!(!payments.is_empty(), "payments must not be empty");
243243

244-
let mut total: i128 = 0;
244+
// Phase 1: Validate all invoices and group totals by token
245+
let mut token_totals: Map<Address, i128> = Map::new(&env);
245246
for p in payments.iter() {
246247
let inv = load_invoice(&env, p.invoice_id);
247248
assert!(inv.status == InvoiceStatus::Pending, "invoice is not pending");
248249
assert!(p.amount > 0, "payment amount must be positive");
249250
let inv_total: i128 = inv.amounts.iter().sum();
250251
assert!(inv.funded + p.amount <= inv_total, "payment exceeds remaining balance");
251-
total += p.amount;
252+
let token = inv.tokens.get(0).expect("no token");
253+
let prev = token_totals.get(token.clone()).unwrap_or(0);
254+
token_totals.set(token, prev + p.amount);
252255
}
253256

254-
let first = load_invoice(&env, payments.get(0).unwrap().invoice_id);
255-
let token_client = token::Client::new(&env, &first.tokens.get(0).expect("no token"));
256-
token_client.transfer(&payer, &env.current_contract_address(), &total);
257+
// Phase 2: Transfer tokens — one transfer per unique token
258+
for (token, amount) in token_totals.iter() {
259+
let token_client = token::Client::new(&env, &token);
260+
token_client.transfer(&payer, &env.current_contract_address(), &amount);
261+
}
257262

263+
// Phase 3: Update each invoice's state
258264
for p in payments.iter() {
259265
let mut inv = load_invoice(&env, p.invoice_id);
260266
inv.payments.push_back(Payment { payer: payer.clone(), amount: p.amount, tip: 0 });
261267
inv.funded += p.amount;
262268
append_audit(&env, p.invoice_id, symbol_short!("pool_pay"), &payer);
263269
events::payment_received(&env, p.invoice_id, &payer, p.amount);
264270
let inv_total: i128 = inv.amounts.iter().sum();
265-
if inv.funded >= inv_total && !inv.escrow_enabled {
266-
Self::_release(&env, p.invoice_id, &mut inv, &payer);
271+
if inv.funded >= inv_total {
272+
if inv.escrow_enabled {
273+
let release_at = env.ledger().timestamp() + inv.escrow_release_delay;
274+
env.storage().persistent().set(&escrow_state_key(p.invoice_id), &release_at);
275+
save_invoice(&env, p.invoice_id, &inv);
276+
} else {
277+
Self::_release(&env, p.invoice_id, &mut inv, &payer);
278+
}
267279
} else {
268280
save_invoice(&env, p.invoice_id, &inv);
269281
}

contracts/sharpy/src/test.rs

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
#[cfg(test)]
22
mod tests {
3-
use soroban_sdk::{testutils::Address as _, Address, Env, Vec};
3+
use soroban_sdk::{testutils::Address as _, token, Address, Env, Vec};
44
use soroban_sdk::testutils::Ledger as _;
55
use crate::{
66
types::{CreateInvoiceParams, InvoiceOptions, InvoicePayment, InvoiceStatus, SplitRule},
77
SharpyContractClient,
88
};
99

10+
fn setup_with_tokens(
11+
env: &Env,
12+
payer: &Address,
13+
amounts: &[i128],
14+
) -> (Address, Address) {
15+
let admin = Address::generate(env);
16+
let token_a = env.register_stellar_asset_contract(admin.clone());
17+
let token_b = env.register_stellar_asset_contract(admin);
18+
let sac_a = token::StellarAssetClient::new(env, &token_a);
19+
let sac_b = token::StellarAssetClient::new(env, &token_b);
20+
sac_a.mint(payer, &amounts[0]);
21+
sac_b.mint(payer, &amounts[1]);
22+
(token_a, token_b)
23+
}
24+
1025
fn setup() -> (Env, SharpyContractClient<'static>) {
1126
let env = Env::default();
1227
env.mock_all_auths();
@@ -390,4 +405,127 @@ mod tests {
390405
&default_options(&env),
391406
);
392407
}
408+
409+
#[test]
410+
fn test_pool_pay_multi_token() {
411+
let (env, client) = setup();
412+
let creator = Address::generate(&env);
413+
let payer = Address::generate(&env);
414+
let recipient = Address::generate(&env);
415+
let (token_a, token_b) = setup_with_tokens(&env, &payer, &[1000i128, 1000i128]);
416+
let deadline = env.ledger().timestamp() + 86400;
417+
418+
let id1 = client.create_invoice(
419+
&creator,
420+
&Vec::from_array(&env, [recipient.clone()]),
421+
&Vec::from_array(&env, [500i128]),
422+
&Vec::from_array(&env, [token_a.clone()]),
423+
&deadline,
424+
&default_options(&env),
425+
);
426+
let id2 = client.create_invoice(
427+
&creator,
428+
&Vec::from_array(&env, [recipient.clone()]),
429+
&Vec::from_array(&env, [300i128]),
430+
&Vec::from_array(&env, [token_b.clone()]),
431+
&deadline,
432+
&default_options(&env),
433+
);
434+
435+
let payments = Vec::from_array(&env, [
436+
InvoicePayment { invoice_id: id1, amount: 500i128 },
437+
InvoicePayment { invoice_id: id2, amount: 300i128 },
438+
]);
439+
client.pool_pay(&payer, &payments);
440+
441+
let inv1 = client.get_invoice(&id1);
442+
assert_eq!(inv1.funded, 500i128);
443+
assert_eq!(inv1.status, InvoiceStatus::Released);
444+
445+
let inv2 = client.get_invoice(&id2);
446+
assert_eq!(inv2.funded, 300i128);
447+
assert_eq!(inv2.status, InvoiceStatus::Released);
448+
}
449+
450+
#[test]
451+
fn test_pool_pay_multi_token_same_token_grouped() {
452+
let (env, client) = setup();
453+
let creator = Address::generate(&env);
454+
let payer = Address::generate(&env);
455+
let recipient = Address::generate(&env);
456+
let (token, _) = setup_with_tokens(&env, &payer, &[1000, 1000]);
457+
let deadline = env.ledger().timestamp() + 86400;
458+
459+
let id1 = client.create_invoice(
460+
&creator,
461+
&Vec::from_array(&env, [recipient.clone()]),
462+
&Vec::from_array(&env, [200i128]),
463+
&Vec::from_array(&env, [token.clone()]),
464+
&deadline,
465+
&default_options(&env),
466+
);
467+
let id2 = client.create_invoice(
468+
&creator,
469+
&Vec::from_array(&env, [recipient.clone()]),
470+
&Vec::from_array(&env, [300i128]),
471+
&Vec::from_array(&env, [token.clone()]),
472+
&deadline,
473+
&default_options(&env),
474+
);
475+
476+
let payments = Vec::from_array(&env, [
477+
InvoicePayment { invoice_id: id1, amount: 200i128 },
478+
InvoicePayment { invoice_id: id2, amount: 300i128 },
479+
]);
480+
client.pool_pay(&payer, &payments);
481+
482+
let inv1 = client.get_invoice(&id1);
483+
assert_eq!(inv1.funded, 200i128);
484+
assert_eq!(inv1.status, InvoiceStatus::Released);
485+
486+
let inv2 = client.get_invoice(&id2);
487+
assert_eq!(inv2.funded, 300i128);
488+
assert_eq!(inv2.status, InvoiceStatus::Released);
489+
}
490+
491+
#[test]
492+
fn test_pool_pay_partial_multi_token() {
493+
let (env, client) = setup();
494+
let creator = Address::generate(&env);
495+
let payer = Address::generate(&env);
496+
let recipient = Address::generate(&env);
497+
let (token_a, token_b) = setup_with_tokens(&env, &payer, &[1000i128, 1000i128]);
498+
let deadline = env.ledger().timestamp() + 86400;
499+
500+
let id1 = client.create_invoice(
501+
&creator,
502+
&Vec::from_array(&env, [recipient.clone()]),
503+
&Vec::from_array(&env, [1000i128]),
504+
&Vec::from_array(&env, [token_a.clone()]),
505+
&deadline,
506+
&default_options(&env),
507+
);
508+
let id2 = client.create_invoice(
509+
&creator,
510+
&Vec::from_array(&env, [recipient]),
511+
&Vec::from_array(&env, [1000i128]),
512+
&Vec::from_array(&env, [token_b]),
513+
&deadline,
514+
&default_options(&env),
515+
);
516+
517+
let payments = Vec::from_array(&env, [
518+
InvoicePayment { invoice_id: id1, amount: 400i128 },
519+
InvoicePayment { invoice_id: id2, amount: 600i128 },
520+
]);
521+
client.pool_pay(&payer, &payments);
522+
523+
let inv1 = client.get_invoice(&id1);
524+
assert_eq!(inv1.funded, 400i128);
525+
assert_eq!(inv1.status, InvoiceStatus::Pending);
526+
527+
let inv2 = client.get_invoice(&id2);
528+
assert_eq!(inv2.funded, 600i128);
529+
assert_eq!(inv2.status, InvoiceStatus::Pending);
530+
}
393531
}

0 commit comments

Comments
 (0)