Skip to content

Commit b6b9d4e

Browse files
authored
Merge pull request #17 from Clement-coder/feat/multi-token-support
feat: multi-token support for invoice creation
2 parents da16fd5 + 644283c commit b6b9d4e

3 files changed

Lines changed: 92 additions & 41 deletions

File tree

contracts/sharpy/src/lib.rs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,14 @@ fn build_invoice(
6565
creator: Address,
6666
recipients: Vec<Address>,
6767
amounts: Vec<i128>,
68-
token: Address,
68+
tokens: Vec<Address>,
6969
deadline: u64,
7070
escrow_enabled: bool,
7171
escrow_release_delay: u64,
7272
split_rules: Vec<SplitRule>,
7373
) -> Invoice {
74-
let mut tokens: Vec<Address> = Vec::new(env);
7574
let mut claimed: Vec<i128> = Vec::new(env);
7675
for _ in recipients.iter() {
77-
tokens.push_back(token.clone());
7876
claimed.push_back(0i128);
7977
}
8078
Invoice {
@@ -124,13 +122,14 @@ impl SharpyContract {
124122
creator: Address,
125123
recipients: Vec<Address>,
126124
amounts: Vec<i128>,
127-
token: Address,
125+
tokens: Vec<Address>,
128126
deadline: u64,
129127
options: InvoiceOptions,
130128
) -> u64 {
131129
require_not_paused(&env);
132130
creator.require_auth();
133131
assert_eq!(recipients.len(), amounts.len(), "recipients and amounts length mismatch");
132+
assert_eq!(recipients.len(), tokens.len(), "recipients and tokens length mismatch");
134133
assert!(!recipients.is_empty(), "must have at least one recipient");
135134
assert!(deadline > env.ledger().timestamp(), "deadline must be in the future");
136135
for amt in amounts.iter() {
@@ -139,7 +138,7 @@ impl SharpyContract {
139138

140139
let id = bump_counter(&env);
141140
let invoice = build_invoice(
142-
&env, creator.clone(), recipients, amounts, token, deadline,
141+
&env, creator.clone(), recipients, amounts, tokens, deadline,
143142
options.escrow_enabled, options.escrow_release_delay.unwrap_or(0), options.split_rules,
144143
);
145144
save_invoice(&env, id, &invoice);
@@ -154,10 +153,11 @@ impl SharpyContract {
154153

155154
let mut ids: Vec<u64> = Vec::new(&env);
156155
for params in invoices.iter() {
156+
assert_eq!(params.recipients.len(), params.tokens.len(), "recipients and tokens length mismatch");
157157
let id = bump_counter(&env);
158158
let invoice = build_invoice(
159159
&env, creator.clone(), params.recipients.clone(), params.amounts.clone(),
160-
params.token.clone(), params.deadline, false, 0, Vec::new(&env),
160+
params.tokens.clone(), params.deadline, false, 0, Vec::new(&env),
161161
);
162162
save_invoice(&env, id, &invoice);
163163
events::invoice_created(&env, id, &creator);
@@ -171,30 +171,29 @@ impl SharpyContract {
171171
creator: Address,
172172
recipients: Vec<Address>,
173173
amounts: Vec<i128>,
174-
token: Address,
174+
tokens: Vec<Address>,
175175
deadline: u64,
176176
recurrence_interval: u64,
177177
max_recurrences: u32,
178178
) -> u64 {
179179
require_not_paused(&env);
180180
creator.require_auth();
181181
assert_eq!(recipients.len(), amounts.len(), "recipients and amounts length mismatch");
182+
assert_eq!(recipients.len(), tokens.len(), "recipients and tokens length mismatch");
182183
assert!(recurrence_interval > 0, "recurrence_interval must be positive");
183184

184185
let id = bump_counter(&env);
185186
let invoice = build_invoice(
186187
&env, creator.clone(), recipients.clone(), amounts.clone(),
187-
token.clone(), deadline, false, 0, Vec::new(&env),
188+
tokens.clone(), deadline, false, 0, Vec::new(&env),
188189
);
189190
save_invoice(&env, id, &invoice);
190191

191-
let mut token_vec: Vec<Address> = Vec::new(&env);
192-
token_vec.push_back(token);
193192
let params = SubscriptionParams {
194193
creator: creator.clone(),
195194
recipients,
196195
amounts,
197-
tokens: token_vec,
196+
tokens,
198197
recurrence_interval,
199198
max_recurrences,
200199
num_created: 1,
@@ -286,14 +285,14 @@ impl SharpyContract {
286285
fn _release(env: &Env, invoice_id: u64, invoice: &mut Invoice, actor: &Address) {
287286
assert!(invoice.status == InvoiceStatus::Pending, "invoice is not pending");
288287

289-
let token_client = token::Client::new(env, &invoice.tokens.get(0).expect("no token"));
290288
let total: i128 = invoice.amounts.iter().sum();
291289
let n = invoice.recipients.len();
292290
let mut distributed: i128 = 0;
293291

294292
for i in 0..n {
295293
let recipient = invoice.recipients.get(i).unwrap();
296294
let amount = invoice.amounts.get(i).unwrap();
295+
let token_client = token::Client::new(env, &invoice.tokens.get(i).expect("no token"));
297296

298297
let proportional = if !invoice.split_rules.is_empty() {
299298
match invoice.split_rules.get(i as u32).unwrap() {
@@ -333,12 +332,11 @@ impl SharpyContract {
333332
{
334333
if params.max_recurrences == 0 || params.num_created < params.max_recurrences {
335334
let next_deadline = env.ledger().timestamp() + params.recurrence_interval;
336-
let token = params.tokens.get(0).expect("no token");
337335
let next_id = bump_counter(env);
338336

339337
let next_invoice = build_invoice(
340338
env, params.creator.clone(), params.recipients.clone(),
341-
params.amounts.clone(), token, next_deadline, false, 0, Vec::new(env),
339+
params.amounts.clone(), params.tokens.clone(), next_deadline, false, 0, Vec::new(env),
342340
);
343341
save_invoice(env, next_id, &next_invoice);
344342

contracts/sharpy/src/test.rs

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ mod tests {
4343
&creator,
4444
&Vec::from_array(&env, [recipient]),
4545
&Vec::from_array(&env, [1000i128]),
46-
&token,
46+
&Vec::from_array(&env, [token]),
4747
&deadline,
4848
&default_options(&env),
4949
);
@@ -65,7 +65,7 @@ mod tests {
6565
let params = CreateInvoiceParams {
6666
recipients: Vec::from_array(&env, [recipient.clone()]),
6767
amounts: Vec::from_array(&env, [500i128]),
68-
token: token.clone(),
68+
tokens: Vec::from_array(&env, [token.clone()]),
6969
deadline,
7070
};
7171
let batch = Vec::from_array(&env, [params.clone(), params]);
@@ -86,7 +86,7 @@ mod tests {
8686
&creator,
8787
&Vec::from_array(&env, [recipient]),
8888
&Vec::from_array(&env, [1000i128]),
89-
&token,
89+
&Vec::from_array(&env, [token]),
9090
&deadline,
9191
&default_options(&env),
9292
);
@@ -109,9 +109,11 @@ mod tests {
109109
let deadline = env.ledger().timestamp() + 86400;
110110

111111
let id1 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient.clone()]),
112-
&Vec::from_array(&env, [100i128]), &token, &deadline, &default_options(&env));
112+
&Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [token.clone()]),
113+
&deadline, &default_options(&env));
113114
let id2 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
114-
&Vec::from_array(&env, [100i128]), &token, &deadline, &default_options(&env));
115+
&Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [token]),
116+
&deadline, &default_options(&env));
115117

116118
assert_eq!(id2, id1 + 1);
117119
}
@@ -125,7 +127,8 @@ mod tests {
125127
let deadline = env.ledger().timestamp() + 86400;
126128

127129
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient.clone()]),
128-
&Vec::from_array(&env, [750i128]), &token, &deadline, &default_options(&env));
130+
&Vec::from_array(&env, [750i128]), &Vec::from_array(&env, [token]),
131+
&deadline, &default_options(&env));
129132

130133
let invoice = client.get_invoice(&id);
131134
assert_eq!(invoice.creator, creator);
@@ -144,14 +147,13 @@ mod tests {
144147
let params = CreateInvoiceParams {
145148
recipients: Vec::from_array(&env, [recipient]),
146149
amounts: Vec::from_array(&env, [100i128]),
147-
token,
150+
tokens: Vec::from_array(&env, [token]),
148151
deadline,
149152
};
150153
let batch = Vec::from_array(&env, [params.clone(), params.clone(), params]);
151154
let ids = client.create_batch(&creator, &batch);
152155

153156
assert_eq!(ids.len(), 3);
154-
// IDs should be sequential
155157
let id0 = ids.get(0).unwrap();
156158
let id1 = ids.get(1).unwrap();
157159
let id2 = ids.get(2).unwrap();
@@ -168,7 +170,8 @@ mod tests {
168170
let deadline = env.ledger().timestamp() + 86400;
169171

170172
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
171-
&Vec::from_array(&env, [500i128]), &token, &deadline, &default_options(&env));
173+
&Vec::from_array(&env, [500i128]), &Vec::from_array(&env, [token]),
174+
&deadline, &default_options(&env));
172175

173176
client.cancel_invoice(&creator, &id);
174177
let log = client.get_audit_log(&id);
@@ -184,12 +187,11 @@ mod tests {
184187
let deadline = env.ledger().timestamp() + 86400;
185188

186189
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
187-
&Vec::from_array(&env, [500i128]), &token, &deadline, &default_options(&env));
190+
&Vec::from_array(&env, [500i128]), &Vec::from_array(&env, [token]),
191+
&deadline, &default_options(&env));
188192

189-
// Simulate funded > 0 by patching: we test status is Cancelled when funded == 0
190193
client.cancel_invoice(&creator, &id);
191194
let invoice = client.get_invoice(&id);
192-
// funded was 0, so status should be Cancelled not Refunded
193195
assert_eq!(invoice.status, InvoiceStatus::Cancelled);
194196
}
195197

@@ -205,10 +207,10 @@ mod tests {
205207
&creator,
206208
&Vec::from_array(&env, [recipient]),
207209
&Vec::from_array(&env, [1000i128]),
208-
&token,
210+
&Vec::from_array(&env, [token]),
209211
&deadline,
210-
&(86400u64 * 30), // 30 day interval
211-
&0u32, // infinite
212+
&(86400u64 * 30),
213+
&0u32,
212214
);
213215

214216
let invoice = client.get_invoice(&id);
@@ -228,13 +230,12 @@ mod tests {
228230
&creator,
229231
&Vec::from_array(&env, [recipient]),
230232
&Vec::from_array(&env, [500i128]),
231-
&token,
233+
&Vec::from_array(&env, [token]),
232234
&deadline,
233235
&(86400u64),
234236
&0u32,
235237
);
236238

237-
// Before release, no next invoice exists
238239
assert!(client.get_next_recurring(&id).is_none());
239240
}
240241

@@ -244,10 +245,11 @@ mod tests {
244245
let creator = Address::generate(&env);
245246
let recipient = Address::generate(&env);
246247
let token = Address::generate(&env);
247-
let deadline = env.ledger().timestamp() + 7 * 86400; // 7 days
248+
let deadline = env.ledger().timestamp() + 7 * 86400;
248249

249250
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
250-
&Vec::from_array(&env, [100i128]), &token, &deadline, &default_options(&env));
251+
&Vec::from_array(&env, [100i128]), &Vec::from_array(&env, [token]),
252+
&deadline, &default_options(&env));
251253

252254
let invoice = client.get_invoice(&id);
253255
assert_eq!(invoice.deadline, deadline);
@@ -269,7 +271,8 @@ mod tests {
269271
};
270272

271273
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
272-
&Vec::from_array(&env, [1000i128]), &token, &deadline, &options);
274+
&Vec::from_array(&env, [1000i128]), &Vec::from_array(&env, [token]),
275+
&deadline, &options);
273276

274277
let invoice = client.get_invoice(&id);
275278
assert!(invoice.escrow_enabled);
@@ -283,21 +286,24 @@ mod tests {
283286
let r1 = Address::generate(&env);
284287
let r2 = Address::generate(&env);
285288
let r3 = Address::generate(&env);
286-
let token = Address::generate(&env);
289+
let t1 = Address::generate(&env);
290+
let t2 = Address::generate(&env);
291+
let t3 = Address::generate(&env);
287292
let deadline = env.ledger().timestamp() + 86400;
288293

289294
let id = client.create_invoice(
290295
&creator,
291296
&Vec::from_array(&env, [r1.clone(), r2.clone(), r3.clone()]),
292297
&Vec::from_array(&env, [300i128, 300i128, 400i128]),
293-
&token,
298+
&Vec::from_array(&env, [t1, t2, t3]),
294299
&deadline,
295300
&default_options(&env),
296301
);
297302

298303
let invoice = client.get_invoice(&id);
299304
assert_eq!(invoice.recipients.len(), 3);
300305
assert_eq!(invoice.amounts.get(2).unwrap(), 400i128);
306+
assert_eq!(invoice.tokens.len(), 3);
301307
}
302308

303309
#[test]
@@ -310,7 +316,8 @@ mod tests {
310316
let deadline = env.ledger().timestamp() + 86400;
311317

312318
let id = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
313-
&Vec::from_array(&env, [500i128]), &token, &deadline, &default_options(&env));
319+
&Vec::from_array(&env, [500i128]), &Vec::from_array(&env, [token]),
320+
&deadline, &default_options(&env));
314321

315322
assert_eq!(client.get_payer_total(&id, &payer), 0i128);
316323
}
@@ -326,15 +333,61 @@ mod tests {
326333
let deadline = env.ledger().timestamp() + 86400;
327334

328335
let id1 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient.clone()]),
329-
&Vec::from_array(&env, [200i128]), &token, &deadline, &default_options(&env));
336+
&Vec::from_array(&env, [200i128]), &Vec::from_array(&env, [token.clone()]),
337+
&deadline, &default_options(&env));
330338
let id2 = client.create_invoice(&creator, &Vec::from_array(&env, [recipient]),
331-
&Vec::from_array(&env, [300i128]), &token, &deadline, &default_options(&env));
339+
&Vec::from_array(&env, [300i128]), &Vec::from_array(&env, [token]),
340+
&deadline, &default_options(&env));
332341

333-
// Overpayment on id1 should panic
334342
let payments = Vec::from_array(&env, [
335343
InvoicePayment { invoice_id: id1, amount: 999i128 },
336344
InvoicePayment { invoice_id: id2, amount: 100i128 },
337345
]);
338346
client.pool_pay(&payer, &payments);
339347
}
348+
349+
#[test]
350+
fn test_multi_token_invoice_stores_per_recipient_tokens() {
351+
let (env, client) = setup();
352+
let creator = Address::generate(&env);
353+
let r1 = Address::generate(&env);
354+
let r2 = Address::generate(&env);
355+
let usdc = Address::generate(&env);
356+
let xlm = Address::generate(&env);
357+
let deadline = env.ledger().timestamp() + 86400;
358+
359+
let id = client.create_invoice(
360+
&creator,
361+
&Vec::from_array(&env, [r1, r2]),
362+
&Vec::from_array(&env, [500i128, 300i128]),
363+
&Vec::from_array(&env, [usdc.clone(), xlm.clone()]),
364+
&deadline,
365+
&default_options(&env),
366+
);
367+
368+
let invoice = client.get_invoice(&id);
369+
assert_eq!(invoice.tokens.get(0).unwrap(), usdc);
370+
assert_eq!(invoice.tokens.get(1).unwrap(), xlm);
371+
}
372+
373+
#[test]
374+
#[should_panic]
375+
fn test_create_invoice_rejects_token_length_mismatch() {
376+
let (env, client) = setup();
377+
let creator = Address::generate(&env);
378+
let r1 = Address::generate(&env);
379+
let r2 = Address::generate(&env);
380+
let token = Address::generate(&env);
381+
let deadline = env.ledger().timestamp() + 86400;
382+
383+
// 2 recipients but only 1 token — should panic
384+
client.create_invoice(
385+
&creator,
386+
&Vec::from_array(&env, [r1, r2]),
387+
&Vec::from_array(&env, [500i128, 300i128]),
388+
&Vec::from_array(&env, [token]),
389+
&deadline,
390+
&default_options(&env),
391+
);
392+
}
340393
}

contracts/sharpy/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub struct InvoiceOptions {
8787
pub struct CreateInvoiceParams {
8888
pub recipients: Vec<Address>,
8989
pub amounts: Vec<i128>,
90-
pub token: Address,
90+
pub tokens: Vec<Address>,
9191
pub deadline: u64,
9292
}
9393

0 commit comments

Comments
 (0)