Skip to content
Merged
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: 12 additions & 14 deletions contracts/sharpy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,14 @@ fn build_invoice(
creator: Address,
recipients: Vec<Address>,
amounts: Vec<i128>,
token: Address,
tokens: Vec<Address>,
deadline: u64,
escrow_enabled: bool,
escrow_release_delay: u64,
split_rules: Vec<SplitRule>,
) -> Invoice {
let mut tokens: Vec<Address> = Vec::new(env);
let mut claimed: Vec<i128> = Vec::new(env);
for _ in recipients.iter() {
tokens.push_back(token.clone());
claimed.push_back(0i128);
}
Invoice {
Expand Down Expand Up @@ -124,13 +122,14 @@ impl SharpyContract {
creator: Address,
recipients: Vec<Address>,
amounts: Vec<i128>,
token: Address,
tokens: Vec<Address>,
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() {
Expand All @@ -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);
Expand All @@ -154,10 +153,11 @@ impl SharpyContract {

let mut ids: Vec<u64> = 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);
Expand All @@ -171,30 +171,29 @@ impl SharpyContract {
creator: Address,
recipients: Vec<Address>,
amounts: Vec<i128>,
token: Address,
tokens: Vec<Address>,
deadline: u64,
recurrence_interval: u64,
max_recurrences: u32,
) -> 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!(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<Address> = 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,
Expand Down Expand Up @@ -286,14 +285,14 @@ 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;

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() {
Expand Down Expand Up @@ -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);

Expand Down
105 changes: 79 additions & 26 deletions contracts/sharpy/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Expand All @@ -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]);
Expand All @@ -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),
);
Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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);
}

Expand All @@ -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);
Expand All @@ -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());
}

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -283,21 +286,24 @@ 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),
);

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]
Expand All @@ -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);
}
Expand All @@ -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),
);
}
}
2 changes: 1 addition & 1 deletion contracts/sharpy/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub struct InvoiceOptions {
pub struct CreateInvoiceParams {
pub recipients: Vec<Address>,
pub amounts: Vec<i128>,
pub token: Address,
pub tokens: Vec<Address>,
pub deadline: u64,
}

Expand Down
Loading