diff --git a/contracts/sharpy/src/lib.rs b/contracts/sharpy/src/lib.rs
index 54e4e87..7cd864d 100644
--- a/contracts/sharpy/src/lib.rs
+++ b/contracts/sharpy/src/lib.rs
@@ -65,16 +65,14 @@ fn build_invoice(
creator: Address,
recipients: Vec
,
amounts: Vec,
- token: Address,
+ tokens: Vec,
deadline: u64,
escrow_enabled: bool,
escrow_release_delay: u64,
split_rules: Vec,
) -> Invoice {
- let mut tokens: Vec = Vec::new(env);
let mut claimed: Vec = Vec::new(env);
for _ in recipients.iter() {
- tokens.push_back(token.clone());
claimed.push_back(0i128);
}
Invoice {
@@ -124,13 +122,14 @@ impl SharpyContract {
creator: Address,
recipients: Vec,
amounts: Vec,
- token: Address,
+ tokens: Vec,
deadline: u64,
options: InvoiceOptions,
) -> u64 {
require_not_paused(&env);
creator.require_auth();
assert_eq!(recipients.len(), amounts.len(), "recipients and amounts length mismatch");
+ assert_eq!(recipients.len(), tokens.len(), "recipients and tokens length mismatch");
assert!(!recipients.is_empty(), "must have at least one recipient");
assert!(deadline > env.ledger().timestamp(), "deadline must be in the future");
for amt in amounts.iter() {
@@ -139,7 +138,7 @@ impl SharpyContract {
let id = bump_counter(&env);
let invoice = build_invoice(
- &env, creator.clone(), recipients, amounts, token, deadline,
+ &env, creator.clone(), recipients, amounts, tokens, deadline,
options.escrow_enabled, options.escrow_release_delay.unwrap_or(0), options.split_rules,
);
save_invoice(&env, id, &invoice);
@@ -154,10 +153,11 @@ impl SharpyContract {
let mut ids: Vec = Vec::new(&env);
for params in invoices.iter() {
+ assert_eq!(params.recipients.len(), params.tokens.len(), "recipients and tokens length mismatch");
let id = bump_counter(&env);
let invoice = build_invoice(
&env, creator.clone(), params.recipients.clone(), params.amounts.clone(),
- params.token.clone(), params.deadline, false, 0, Vec::new(&env),
+ params.tokens.clone(), params.deadline, false, 0, Vec::new(&env),
);
save_invoice(&env, id, &invoice);
events::invoice_created(&env, id, &creator);
@@ -171,7 +171,7 @@ impl SharpyContract {
creator: Address,
recipients: Vec,
amounts: Vec,
- token: Address,
+ tokens: Vec,
deadline: u64,
recurrence_interval: u64,
max_recurrences: u32,
@@ -179,22 +179,21 @@ impl SharpyContract {
require_not_paused(&env);
creator.require_auth();
assert_eq!(recipients.len(), amounts.len(), "recipients and amounts length mismatch");
+ assert_eq!(recipients.len(), tokens.len(), "recipients and tokens length mismatch");
assert!(recurrence_interval > 0, "recurrence_interval must be positive");
let id = bump_counter(&env);
let invoice = build_invoice(
&env, creator.clone(), recipients.clone(), amounts.clone(),
- token.clone(), deadline, false, 0, Vec::new(&env),
+ tokens.clone(), deadline, false, 0, Vec::new(&env),
);
save_invoice(&env, id, &invoice);
- let mut token_vec: Vec = Vec::new(&env);
- token_vec.push_back(token);
let params = SubscriptionParams {
creator: creator.clone(),
recipients,
amounts,
- tokens: token_vec,
+ tokens,
recurrence_interval,
max_recurrences,
num_created: 1,
@@ -286,7 +285,6 @@ impl SharpyContract {
fn _release(env: &Env, invoice_id: u64, invoice: &mut Invoice, actor: &Address) {
assert!(invoice.status == InvoiceStatus::Pending, "invoice is not pending");
- let token_client = token::Client::new(env, &invoice.tokens.get(0).expect("no token"));
let total: i128 = invoice.amounts.iter().sum();
let n = invoice.recipients.len();
let mut distributed: i128 = 0;
@@ -294,6 +292,7 @@ impl SharpyContract {
for i in 0..n {
let recipient = invoice.recipients.get(i).unwrap();
let amount = invoice.amounts.get(i).unwrap();
+ let token_client = token::Client::new(env, &invoice.tokens.get(i).expect("no token"));
let proportional = if !invoice.split_rules.is_empty() {
match invoice.split_rules.get(i as u32).unwrap() {
@@ -333,12 +332,11 @@ impl SharpyContract {
{
if params.max_recurrences == 0 || params.num_created < params.max_recurrences {
let next_deadline = env.ledger().timestamp() + params.recurrence_interval;
- let token = params.tokens.get(0).expect("no token");
let next_id = bump_counter(env);
let next_invoice = build_invoice(
env, params.creator.clone(), params.recipients.clone(),
- params.amounts.clone(), token, next_deadline, false, 0, Vec::new(env),
+ params.amounts.clone(), params.tokens.clone(), next_deadline, false, 0, Vec::new(env),
);
save_invoice(env, next_id, &next_invoice);
diff --git a/contracts/sharpy/src/test.rs b/contracts/sharpy/src/test.rs
index 3cfaf5e..7b7395f 100644
--- a/contracts/sharpy/src/test.rs
+++ b/contracts/sharpy/src/test.rs
@@ -43,7 +43,7 @@ mod tests {
&creator,
&Vec::from_array(&env, [recipient]),
&Vec::from_array(&env, [1000i128]),
- &token,
+ &Vec::from_array(&env, [token]),
&deadline,
&default_options(&env),
);
@@ -65,7 +65,7 @@ mod tests {
let params = CreateInvoiceParams {
recipients: Vec::from_array(&env, [recipient.clone()]),
amounts: Vec::from_array(&env, [500i128]),
- token: token.clone(),
+ tokens: Vec::from_array(&env, [token.clone()]),
deadline,
};
let batch = Vec::from_array(&env, [params.clone(), params]);
@@ -86,7 +86,7 @@ mod tests {
&creator,
&Vec::from_array(&env, [recipient]),
&Vec::from_array(&env, [1000i128]),
- &token,
+ &Vec::from_array(&env, [token]),
&deadline,
&default_options(&env),
);
@@ -109,9 +109,11 @@ mod tests {
let deadline = env.ledger().timestamp() + 86400;
let id1 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient.clone()]),
- &Vec::from_array(&env, [100i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [token.clone()]),
+ &deadline, &default_options(&env));
let id2 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [100i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
assert_eq!(id2, id1 + 1);
}
@@ -125,7 +127,8 @@ mod tests {
let deadline = env.ledger().timestamp() + 86400;
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient.clone()]),
- &Vec::from_array(&env, [750i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [750i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
let invoice = client.get_invoice(&id);
assert_eq!(invoice.creator, creator);
@@ -144,14 +147,13 @@ mod tests {
let params = CreateInvoiceParams {
recipients: Vec::from_array(&env, [recipient]),
amounts: Vec::from_array(&env, [100i128]),
- token,
+ tokens: Vec::from_array(&env, [token]),
deadline,
};
let batch = Vec::from_array(&env, [params.clone(), params.clone(), params]);
let ids = client.create_batch(&creator, &batch);
assert_eq!(ids.len(), 3);
- // IDs should be sequential
let id0 = ids.get(0).unwrap();
let id1 = ids.get(1).unwrap();
let id2 = ids.get(2).unwrap();
@@ -168,7 +170,8 @@ mod tests {
let deadline = env.ledger().timestamp() + 86400;
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [500i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [500i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
client.cancel_invoice(&creator, &id);
let log = client.get_audit_log(&id);
@@ -184,12 +187,11 @@ mod tests {
let deadline = env.ledger().timestamp() + 86400;
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [500i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [500i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
- // Simulate funded > 0 by patching: we test status is Cancelled when funded == 0
client.cancel_invoice(&creator, &id);
let invoice = client.get_invoice(&id);
- // funded was 0, so status should be Cancelled not Refunded
assert_eq!(invoice.status, InvoiceStatus::Cancelled);
}
@@ -205,10 +207,10 @@ mod tests {
&creator,
&Vec::from_array(&env, [recipient]),
&Vec::from_array(&env, [1000i128]),
- &token,
+ &Vec::from_array(&env, [token]),
&deadline,
- &(86400u64 * 30), // 30 day interval
- &0u32, // infinite
+ &(86400u64 * 30),
+ &0u32,
);
let invoice = client.get_invoice(&id);
@@ -228,13 +230,12 @@ mod tests {
&creator,
&Vec::from_array(&env, [recipient]),
&Vec::from_array(&env, [500i128]),
- &token,
+ &Vec::from_array(&env, [token]),
&deadline,
&(86400u64),
&0u32,
);
- // Before release, no next invoice exists
assert!(client.get_next_recurring(&id).is_none());
}
@@ -244,10 +245,11 @@ mod tests {
let creator = Address::generate(&env);
let recipient = Address::generate(&env);
let token = Address::generate(&env);
- let deadline = env.ledger().timestamp() + 7 * 86400; // 7 days
+ let deadline = env.ledger().timestamp() + 7 * 86400;
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [100i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
let invoice = client.get_invoice(&id);
assert_eq!(invoice.deadline, deadline);
@@ -269,7 +271,8 @@ mod tests {
};
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [1000i128]), &token, &deadline, &options);
+ &Vec::from_array(&env, [1000i128]), &Vec::from_array(&env, [token]),
+ &deadline, &options);
let invoice = client.get_invoice(&id);
assert!(invoice.escrow_enabled);
@@ -283,14 +286,16 @@ mod tests {
let r1 = Address::generate(&env);
let r2 = Address::generate(&env);
let r3 = Address::generate(&env);
- let token = Address::generate(&env);
+ let t1 = Address::generate(&env);
+ let t2 = Address::generate(&env);
+ let t3 = Address::generate(&env);
let deadline = env.ledger().timestamp() + 86400;
let id = client.create_invoice(
&creator,
&Vec::from_array(&env, [r1.clone(), r2.clone(), r3.clone()]),
&Vec::from_array(&env, [300i128, 300i128, 400i128]),
- &token,
+ &Vec::from_array(&env, [t1, t2, t3]),
&deadline,
&default_options(&env),
);
@@ -298,6 +303,7 @@ mod tests {
let invoice = client.get_invoice(&id);
assert_eq!(invoice.recipients.len(), 3);
assert_eq!(invoice.amounts.get(2).unwrap(), 400i128);
+ assert_eq!(invoice.tokens.len(), 3);
}
#[test]
@@ -310,7 +316,8 @@ mod tests {
let deadline = env.ledger().timestamp() + 86400;
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [500i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [500i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
assert_eq!(client.get_payer_total(&id, &payer), 0i128);
}
@@ -326,15 +333,61 @@ mod tests {
let deadline = env.ledger().timestamp() + 86400;
let id1 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient.clone()]),
- &Vec::from_array(&env, [200i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [200i128]), &Vec::from_array(&env, [token.clone()]),
+ &deadline, &default_options(&env));
let id2 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
- &Vec::from_array(&env, [300i128]), &token, &deadline, &default_options(&env));
+ &Vec::from_array(&env, [300i128]), &Vec::from_array(&env, [token]),
+ &deadline, &default_options(&env));
- // Overpayment on id1 should panic
let payments = Vec::from_array(&env, [
InvoicePayment { invoice_id: id1, amount: 999i128 },
InvoicePayment { invoice_id: id2, amount: 100i128 },
]);
client.pool_pay(&payer, &payments);
}
+
+ #[test]
+ fn test_multi_token_invoice_stores_per_recipient_tokens() {
+ let (env, client) = setup();
+ let creator = Address::generate(&env);
+ let r1 = Address::generate(&env);
+ let r2 = Address::generate(&env);
+ let usdc = Address::generate(&env);
+ let xlm = Address::generate(&env);
+ let deadline = env.ledger().timestamp() + 86400;
+
+ let id = client.create_invoice(
+ &creator,
+ &Vec::from_array(&env, [r1, r2]),
+ &Vec::from_array(&env, [500i128, 300i128]),
+ &Vec::from_array(&env, [usdc.clone(), xlm.clone()]),
+ &deadline,
+ &default_options(&env),
+ );
+
+ let invoice = client.get_invoice(&id);
+ assert_eq!(invoice.tokens.get(0).unwrap(), usdc);
+ assert_eq!(invoice.tokens.get(1).unwrap(), xlm);
+ }
+
+ #[test]
+ #[should_panic]
+ fn test_create_invoice_rejects_token_length_mismatch() {
+ let (env, client) = setup();
+ let creator = Address::generate(&env);
+ let r1 = Address::generate(&env);
+ let r2 = Address::generate(&env);
+ let token = Address::generate(&env);
+ let deadline = env.ledger().timestamp() + 86400;
+
+ // 2 recipients but only 1 token — should panic
+ client.create_invoice(
+ &creator,
+ &Vec::from_array(&env, [r1, r2]),
+ &Vec::from_array(&env, [500i128, 300i128]),
+ &Vec::from_array(&env, [token]),
+ &deadline,
+ &default_options(&env),
+ );
+ }
}
diff --git a/contracts/sharpy/src/types.rs b/contracts/sharpy/src/types.rs
index 3fe2d3e..39719db 100644
--- a/contracts/sharpy/src/types.rs
+++ b/contracts/sharpy/src/types.rs
@@ -87,7 +87,7 @@ pub struct InvoiceOptions {
pub struct CreateInvoiceParams {
pub recipients: Vec,
pub amounts: Vec,
- pub token: Address,
+ pub tokens: Vec,
pub deadline: u64,
}