1mod error;
2mod types;
3
4use std::collections::{HashMap, HashSet};
5use std::sync::{Arc, Mutex};
6
7use aes_gcm::aead::{Aead, KeyInit};
8use aes_gcm::{Aes256Gcm, Nonce};
9use rand_core::{OsRng, RngCore};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use x25519_dalek::{PublicKey, StaticSecret};
13use zeroize::Zeroizing;
14
15pub use error::{CoinPaymentErrCode, CoinPaymentError, Result};
16pub use types::*;
17
18const CHEQUE_VERSION: u8 = 0;
19const ENCRYPTED_CHEQUE_VERSION: u8 = 0;
20const NONCE_LEN: usize = 12;
21const PUBLIC_KEY_LEN: usize = 32;
22
23#[derive(Clone)]
24pub struct CoinPaymentRuntime {
25 state: Arc<Mutex<State>>,
26}
27
28struct State {
29 now_ms: Timestamp,
30 next_purse_id: PurseId,
31 purses: HashMap<PurseId, PurseRecord>,
32 receivables: HashMap<Receivable, ReceivableRecord>,
33 pending_cheques: HashMap<Receivable, Cheque>,
34 deposited_by_receivable: HashMap<Receivable, Balance>,
35 deposited_cheques: HashSet<[u8; 32]>,
36 return_hints: HashMap<Receivable, Receivable>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40struct StateSnapshot {
41 version: u8,
42 now_ms: Timestamp,
43 next_purse_id: PurseId,
44 purses: Vec<SnapshotPurse>,
45 receivables: Vec<SnapshotReceivable>,
46 pending_cheques: Vec<Cheque>,
47 deposited_by_receivable: Vec<SnapshotDepositedBalance>,
48 #[serde(default)]
49 deposited_cheques: Vec<[u8; 32]>,
50 #[serde(default)]
51 return_hints: Vec<SnapshotReturnHint>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55struct SnapshotPurse {
56 id: PurseId,
57 info: PurseInfo,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61struct SnapshotReceivable {
62 receivable: Receivable,
63 into: PurseId,
64 secret: [u8; 32],
65 channel: Option<TransmissionChannel>,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69struct SnapshotDepositedBalance {
70 receivable: Receivable,
71 balance: Balance,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
75struct SnapshotReturnHint {
76 receivable: Receivable,
77 return_hint: Receivable,
78}
79
80#[derive(Debug, Clone)]
81struct PurseRecord {
82 info: PurseInfo,
83}
84
85#[derive(Clone)]
86struct ReceivableRecord {
87 into: PurseId,
88 secret: StaticSecret,
89 channel: Option<TransmissionChannel>,
90}
91
92#[derive(Debug, Serialize, Deserialize)]
93struct ChequePayload {
94 version: u8,
95 receivable: Receivable,
96 amount: Balance,
97 return_hint: Option<Receivable>,
98}
99
100impl CoinPaymentRuntime {
101 pub fn capability(&self) -> CoinPaymentCapability {
102 CoinPaymentCapability {
103 version: 0,
104 profile: CoinPaymentProfile::Staged,
105 supports_statement_store_channel: true,
106 supports_refund: true,
107 supports_finality: false,
108 }
109 }
110
111 pub fn new(now_ms: Timestamp, main_balance: Balance) -> Self {
112 let mut purses = HashMap::new();
113 purses.insert(
114 MAIN_PURSE,
115 PurseRecord {
116 info: PurseInfo {
117 name: "Main purse".to_string(),
118 created: now_ms,
119 creator: "user-agent".to_string(),
120 balance: main_balance,
121 },
122 },
123 );
124 Self {
125 state: Arc::new(Mutex::new(State {
126 now_ms,
127 next_purse_id: 1,
128 purses,
129 receivables: HashMap::new(),
130 pending_cheques: HashMap::new(),
131 deposited_by_receivable: HashMap::new(),
132 deposited_cheques: HashSet::new(),
133 return_hints: HashMap::new(),
134 })),
135 }
136 }
137
138 pub fn from_state_json(state_json: impl AsRef<str>) -> Result<Self> {
139 let snapshot: StateSnapshot =
140 serde_json::from_str(state_json.as_ref()).map_err(|error| {
141 CoinPaymentError::internal(format!("could not decode coinpayment state: {error}"))
142 })?;
143 State::from_snapshot(snapshot).map(|state| Self {
144 state: Arc::new(Mutex::new(state)),
145 })
146 }
147
148 pub fn export_state_json(&self) -> Result<String> {
149 let state = self.lock_state();
150 serde_json::to_string(&state.to_snapshot()).map_err(|error| {
151 CoinPaymentError::internal(format!("could not encode coinpayment state: {error}"))
152 })
153 }
154
155 pub fn create_purse(
156 &self,
157 product_id: impl Into<ProductId>,
158 name: impl Into<String>,
159 ) -> PurseId {
160 let mut state = self.lock_state();
161 let purse = state.allocate_purse_id();
162 let record = PurseRecord {
163 info: PurseInfo {
164 name: name.into(),
165 created: state.now_ms,
166 creator: product_id.into(),
167 balance: 0,
168 },
169 };
170 state.purses.insert(purse, record);
171 purse
172 }
173
174 pub fn query_purse(&self, purse: PurseId) -> Result<PurseInfo> {
175 let state = self.lock_state();
176 Ok(state.purse(purse)?.info.clone())
177 }
178
179 pub fn rebalance_purse(
180 &self,
181 from: PurseId,
182 to: PurseId,
183 amount: Balance,
184 ) -> Result<Vec<Status>> {
185 let mut state = self.lock_state();
186 state.transfer_balance(from, to, amount)?;
187 Ok(done_statuses(amount))
188 }
189
190 pub fn delete_purse(&self, target: PurseId, drain_into: PurseId) -> Result<Vec<Status>> {
191 if target == MAIN_PURSE {
192 return Err(CoinPaymentError::new(
193 CoinPaymentErrCode::Denied,
194 "MAIN_PURSE cannot be deleted",
195 ));
196 }
197 let mut state = self.lock_state();
198 let amount = state.purse(target)?.info.balance;
199 state.transfer_balance(target, drain_into, amount)?;
200 state.purses.remove(&target);
201 Ok(done_statuses(amount))
202 }
203
204 pub fn create_receivable(&self, into: PurseId) -> Result<Receivable> {
205 let mut state = self.lock_state();
206 state.purse(into)?;
207 let secret = StaticSecret::random_from_rng(OsRng);
208 let receivable = PublicKey::from(&secret).to_bytes();
209 state.receivables.insert(
210 receivable,
211 ReceivableRecord {
212 into,
213 secret,
214 channel: None,
215 },
216 );
217 Ok(receivable)
218 }
219
220 pub fn listen_for(&self, receivable: Receivable) -> Result<TransmissionChannel> {
221 let mut state = self.lock_state();
222 let record = state.receivable_mut(&receivable)?;
223 if let Some(channel) = &record.channel {
224 return Ok(channel.clone());
225 }
226 let channel = TransmissionChannel::Standard {
227 sss_topic: random_32(),
228 };
229 record.channel = Some(channel.clone());
230 Ok(channel)
231 }
232
233 pub fn invoice_for(&self, receiver: Receivable, amount: Balance) -> Result<Invoice> {
234 let handoff = self.listen_for(receiver)?;
235 Ok(Invoice {
236 version: CHEQUE_VERSION,
237 handoff,
238 receiver,
239 amount,
240 })
241 }
242
243 pub fn create_cheque(&self, from: PurseId, to: Receivable, amount: Balance) -> Result<Cheque> {
244 let mut state = self.lock_state();
245 if state.purse(from)?.info.balance < amount {
246 return Err(CoinPaymentError::new(
247 CoinPaymentErrCode::BalanceLow,
248 "source purse balance is too low",
249 ));
250 }
251 let return_secret = StaticSecret::random_from_rng(OsRng);
252 let return_hint = PublicKey::from(&return_secret).to_bytes();
253 state.receivables.insert(
254 return_hint,
255 ReceivableRecord {
256 into: from,
257 secret: return_secret,
258 channel: None,
259 },
260 );
261 state.purse_mut(from)?.info.balance -= amount;
262 drop(state);
263 encrypt_cheque_payload(to, amount, Some(return_hint))
264 }
265
266 pub fn pay_invoice(&self, invoice: &Invoice) -> Result<Cheque> {
267 if invoice.version != CHEQUE_VERSION {
268 return Err(CoinPaymentError::new(
269 CoinPaymentErrCode::BadCoins,
270 "unsupported invoice version",
271 ));
272 }
273 match &invoice.handoff {
274 TransmissionChannel::Standard { .. } => {
275 self.create_cheque(MAIN_PURSE, invoice.receiver, invoice.amount)
276 }
277 }
278 }
279
280 pub fn receive_cheque(&self, cheque: Cheque) -> Result<()> {
281 if cheque.version != CHEQUE_VERSION {
282 return Err(CoinPaymentError::new(
283 CoinPaymentErrCode::BadCoins,
284 "unsupported cheque version",
285 ));
286 }
287 let mut state = self.lock_state();
288 state.pending_cheques.insert(cheque.id, cheque);
289 Ok(())
290 }
291
292 pub fn pending_cheque(&self, receivable: Receivable) -> Option<Cheque> {
293 self.lock_state().pending_cheques.get(&receivable).cloned()
294 }
295
296 pub fn deposit(&self, cheque: Cheque) -> Result<Vec<Status>> {
297 if cheque.version != CHEQUE_VERSION {
298 return Err(CoinPaymentError::new(
299 CoinPaymentErrCode::BadCoins,
300 "unsupported cheque version",
301 ));
302 }
303 let mut state = self.lock_state();
304 let receivable = state.receivable(&cheque.id)?.clone();
305 let payload = decrypt_cheque_payload(&receivable.secret, &cheque)?;
306 if payload.receivable != cheque.id || payload.amount != cheque.amount {
307 return Err(CoinPaymentError::new(
308 CoinPaymentErrCode::BadCoins,
309 "cheque payload does not match public cheque fields",
310 ));
311 }
312 let cheque_hash = cheque_id(&cheque);
313 if state.deposited_cheques.contains(&cheque_hash) {
314 return Ok(vec![failed_status(
315 CoinPaymentErrCode::SnipedCoins,
316 state
317 .deposited_by_receivable
318 .get(&cheque.id)
319 .copied()
320 .unwrap_or(0),
321 )]);
322 }
323 let purse = state.purse_mut(receivable.into)?;
324 purse.info.balance = purse
325 .info
326 .balance
327 .checked_add(payload.amount)
328 .ok_or_else(|| {
329 CoinPaymentError::new(CoinPaymentErrCode::Internal, "purse balance overflow")
330 })?;
331 let deposited = state.deposited_by_receivable.entry(cheque.id).or_insert(0);
332 *deposited = deposited.checked_add(payload.amount).ok_or_else(|| {
333 CoinPaymentError::new(
334 CoinPaymentErrCode::Internal,
335 "deposited receivable balance overflow",
336 )
337 })?;
338 if let Some(return_hint) = payload.return_hint {
339 state.return_hints.insert(cheque.id, return_hint);
340 }
341 state.deposited_cheques.insert(cheque_hash);
342 state.pending_cheques.remove(&cheque.id);
343 Ok(done_statuses(payload.amount))
344 }
345
346 pub fn refund(&self, receivable: Receivable) -> Result<Vec<Status>> {
347 let mut state = self.lock_state();
348 let record = state.receivable(&receivable)?.clone();
349 let amount = *state.deposited_by_receivable.get(&receivable).unwrap_or(&0);
350 if amount == 0 {
351 return Ok(vec![failed_status(
352 CoinPaymentErrCode::ReceivableNotFound,
353 0,
354 )]);
355 }
356 let Some(return_hint) = state.return_hints.get(&receivable).copied() else {
357 return Ok(vec![failed_status(
358 CoinPaymentErrCode::UnsupportedChannel,
359 0,
360 )]);
361 };
362 if let Ok(return_record) = state.receivable(&return_hint).cloned() {
363 state.transfer_balance(record.into, return_record.into, amount)?;
364 } else {
365 let source = state.purse_mut(record.into)?;
366 if source.info.balance < amount {
367 return Err(CoinPaymentError::new(
368 CoinPaymentErrCode::BalanceLow,
369 "source purse balance is too low",
370 ));
371 }
372 source.info.balance -= amount;
373 }
374 state.deposited_by_receivable.insert(receivable, 0);
375 Ok(done_statuses(amount))
376 }
377
378 fn lock_state(&self) -> std::sync::MutexGuard<'_, State> {
379 self.state.lock().unwrap_or_else(|e| e.into_inner())
380 }
381}
382
383impl State {
384 fn from_snapshot(snapshot: StateSnapshot) -> Result<Self> {
385 if snapshot.version != 0 {
386 return Err(CoinPaymentError::internal(
387 "unsupported coinpayment state version",
388 ));
389 }
390
391 let mut purses = HashMap::new();
392 for purse in snapshot.purses {
393 purses.insert(purse.id, PurseRecord { info: purse.info });
394 }
395 if !purses.contains_key(&MAIN_PURSE) {
396 return Err(CoinPaymentError::internal(
397 "coinpayment state is missing MAIN_PURSE",
398 ));
399 }
400
401 let mut receivables = HashMap::new();
402 for receivable in snapshot.receivables {
403 let secret = StaticSecret::from(receivable.secret);
404 let public = PublicKey::from(&secret).to_bytes();
405 if public != receivable.receivable {
406 return Err(CoinPaymentError::internal(
407 "coinpayment state receivable secret does not match public key",
408 ));
409 }
410 if !purses.contains_key(&receivable.into) {
411 return Err(CoinPaymentError::internal(
412 "coinpayment state receivable targets a missing purse",
413 ));
414 }
415 receivables.insert(
416 receivable.receivable,
417 ReceivableRecord {
418 into: receivable.into,
419 secret,
420 channel: receivable.channel,
421 },
422 );
423 }
424
425 let pending_cheques = snapshot
426 .pending_cheques
427 .into_iter()
428 .map(|cheque| (cheque.id, cheque))
429 .collect();
430 let deposited_by_receivable = snapshot
431 .deposited_by_receivable
432 .into_iter()
433 .map(|entry| (entry.receivable, entry.balance))
434 .collect();
435
436 Ok(Self {
437 now_ms: snapshot.now_ms,
438 next_purse_id: snapshot.next_purse_id.max(1),
439 purses,
440 receivables,
441 pending_cheques,
442 deposited_by_receivable,
443 deposited_cheques: snapshot.deposited_cheques.into_iter().collect(),
444 return_hints: snapshot
445 .return_hints
446 .into_iter()
447 .map(|entry| (entry.receivable, entry.return_hint))
448 .collect(),
449 })
450 }
451
452 fn to_snapshot(&self) -> StateSnapshot {
453 StateSnapshot {
454 version: 0,
455 now_ms: self.now_ms,
456 next_purse_id: self.next_purse_id,
457 purses: self
458 .purses
459 .iter()
460 .map(|(id, record)| SnapshotPurse {
461 id: *id,
462 info: record.info.clone(),
463 })
464 .collect(),
465 receivables: self
466 .receivables
467 .iter()
468 .map(|(receivable, record)| SnapshotReceivable {
469 receivable: *receivable,
470 into: record.into,
471 secret: record.secret.to_bytes(),
472 channel: record.channel.clone(),
473 })
474 .collect(),
475 pending_cheques: self.pending_cheques.values().cloned().collect(),
476 deposited_by_receivable: self
477 .deposited_by_receivable
478 .iter()
479 .map(|(receivable, balance)| SnapshotDepositedBalance {
480 receivable: *receivable,
481 balance: *balance,
482 })
483 .collect(),
484 deposited_cheques: self.deposited_cheques.iter().copied().collect(),
485 return_hints: self
486 .return_hints
487 .iter()
488 .map(|(receivable, return_hint)| SnapshotReturnHint {
489 receivable: *receivable,
490 return_hint: *return_hint,
491 })
492 .collect(),
493 }
494 }
495
496 fn allocate_purse_id(&mut self) -> PurseId {
497 loop {
498 let id = random_purse_id();
499 if id != MAIN_PURSE && !self.purses.contains_key(&id) {
500 return id;
501 }
502 }
503 }
504
505 fn purse(&self, purse: PurseId) -> Result<&PurseRecord> {
506 self.purses.get(&purse).ok_or_else(|| {
507 CoinPaymentError::new(CoinPaymentErrCode::PurseNotFound, "purse was not found")
508 })
509 }
510
511 fn purse_mut(&mut self, purse: PurseId) -> Result<&mut PurseRecord> {
512 self.purses.get_mut(&purse).ok_or_else(|| {
513 CoinPaymentError::new(CoinPaymentErrCode::PurseNotFound, "purse was not found")
514 })
515 }
516
517 fn receivable(&self, receivable: &Receivable) -> Result<&ReceivableRecord> {
518 self.receivables.get(receivable).ok_or_else(|| {
519 CoinPaymentError::new(
520 CoinPaymentErrCode::ReceivableNotFound,
521 "receivable was not found",
522 )
523 })
524 }
525
526 fn receivable_mut(&mut self, receivable: &Receivable) -> Result<&mut ReceivableRecord> {
527 self.receivables.get_mut(receivable).ok_or_else(|| {
528 CoinPaymentError::new(
529 CoinPaymentErrCode::ReceivableNotFound,
530 "receivable was not found",
531 )
532 })
533 }
534
535 fn transfer_balance(&mut self, from: PurseId, to: PurseId, amount: Balance) -> Result<()> {
536 if from == to || amount == 0 {
537 self.purse(from)?;
538 self.purse(to)?;
539 return Ok(());
540 }
541 let source_balance = self.purse(from)?.info.balance;
542 if source_balance < amount {
543 return Err(CoinPaymentError::new(
544 CoinPaymentErrCode::BalanceLow,
545 "source purse balance is too low",
546 ));
547 }
548 self.purse_mut(from)?.info.balance -= amount;
549 let target = self.purse_mut(to)?;
550 target.info.balance = target.info.balance.checked_add(amount).ok_or_else(|| {
551 CoinPaymentError::new(CoinPaymentErrCode::Internal, "purse balance overflow")
552 })?;
553 Ok(())
554 }
555}
556
557fn encrypt_cheque_payload(
558 receivable: Receivable,
559 amount: Balance,
560 return_hint: Option<Receivable>,
561) -> Result<Cheque> {
562 let secret = StaticSecret::random_from_rng(OsRng);
563 let ephemeral_public = PublicKey::from(&secret);
564 let receiver_public = PublicKey::from(receivable);
565 let shared = secret.diffie_hellman(&receiver_public);
566 let key = cheque_key(shared.as_bytes(), &receivable, ephemeral_public.as_bytes());
567 let cipher = Aes256Gcm::new_from_slice(&key)
568 .map_err(|_| CoinPaymentError::internal("could not initialize cheque encryption cipher"))?;
569 let nonce_bytes = random_nonce();
570 let nonce = Nonce::from_slice(&nonce_bytes);
571 let payload = ChequePayload {
572 version: CHEQUE_VERSION,
573 receivable,
574 amount,
575 return_hint,
576 };
577 let payload_bytes = Zeroizing::new(serde_json::to_vec(&payload).map_err(|error| {
578 CoinPaymentError::internal(format!("could not encode cheque payload: {error}"))
579 })?);
580 let ciphertext = cipher
581 .encrypt(nonce, payload_bytes.as_ref())
582 .map_err(|_| CoinPaymentError::internal("could not encrypt cheque payload"))?;
583 let mut encrypted_secrets =
584 Vec::with_capacity(1 + PUBLIC_KEY_LEN + NONCE_LEN + ciphertext.len());
585 encrypted_secrets.push(ENCRYPTED_CHEQUE_VERSION);
586 encrypted_secrets.extend_from_slice(ephemeral_public.as_bytes());
587 encrypted_secrets.extend_from_slice(&nonce_bytes);
588 encrypted_secrets.extend_from_slice(&ciphertext);
589 Ok(Cheque {
590 version: CHEQUE_VERSION,
591 id: receivable,
592 amount,
593 encrypted_secrets,
594 })
595}
596
597fn decrypt_cheque_payload(secret: &StaticSecret, cheque: &Cheque) -> Result<ChequePayload> {
598 let encrypted = &cheque.encrypted_secrets;
599 if encrypted.len() <= 1 + PUBLIC_KEY_LEN + NONCE_LEN || encrypted[0] != ENCRYPTED_CHEQUE_VERSION
600 {
601 return Err(CoinPaymentError::new(
602 CoinPaymentErrCode::BadCoins,
603 "invalid cheque encrypted payload",
604 ));
605 }
606 let mut public = [0u8; PUBLIC_KEY_LEN];
607 public.copy_from_slice(&encrypted[1..1 + PUBLIC_KEY_LEN]);
608 let nonce_start = 1 + PUBLIC_KEY_LEN;
609 let nonce_end = nonce_start + NONCE_LEN;
610 let nonce = Nonce::from_slice(&encrypted[nonce_start..nonce_end]);
611 let ciphertext = &encrypted[nonce_end..];
612 let ephemeral_public = PublicKey::from(public);
613 let shared = secret.diffie_hellman(&ephemeral_public);
614 let key = cheque_key(shared.as_bytes(), &cheque.id, &public);
615 let cipher = Aes256Gcm::new_from_slice(&key)
616 .map_err(|_| CoinPaymentError::internal("could not initialize cheque decryption cipher"))?;
617 let payload = cipher.decrypt(nonce, ciphertext).map_err(|_| {
618 CoinPaymentError::new(CoinPaymentErrCode::BadCoins, "could not decrypt cheque")
619 })?;
620 serde_json::from_slice(&payload).map_err(|error| {
621 CoinPaymentError::new(
622 CoinPaymentErrCode::BadCoins,
623 format!("could not decode cheque payload: {error}"),
624 )
625 })
626}
627
628fn cheque_key(shared: &[u8; 32], receivable: &Receivable, ephemeral_public: &[u8; 32]) -> [u8; 32] {
629 let mut hasher = Sha256::new();
630 hasher.update(b"useragent-kit:coinpayment:rfc17:cheque:v0");
631 hasher.update(shared);
632 hasher.update(receivable);
633 hasher.update(ephemeral_public);
634 hasher.finalize().into()
635}
636
637fn cheque_id(cheque: &Cheque) -> [u8; 32] {
638 let mut hasher = Sha256::new();
639 hasher.update(b"useragent-kit:coinpayment:rfc17:cheque-id:v0");
640 hasher.update([cheque.version]);
641 hasher.update(cheque.id);
642 hasher.update(cheque.amount.to_le_bytes());
643 hasher.update(&cheque.encrypted_secrets);
644 hasher.finalize().into()
645}
646
647fn done_statuses(amount: Balance) -> Vec<Status> {
648 vec![
649 Status::Clearing {
650 clearing: amount,
651 cleared: 0,
652 },
653 Status::Done {
654 cleared: amount,
655 reference: clearing_reference(amount),
656 },
657 ]
658}
659
660fn failed_status(error: CoinPaymentErrCode, cleared: Balance) -> Status {
661 Status::Failed {
662 error: error.as_str().to_string(),
663 cleared,
664 reference: clearing_reference(cleared),
665 }
666}
667
668fn clearing_reference(amount: Balance) -> ClearingReference {
669 let mut root_hasher = Sha256::new();
670 root_hasher.update(b"useragent-kit:coinpayment:rfc17:clearing");
671 root_hasher.update(amount.to_le_bytes());
672 let root: [u8; 32] = root_hasher.finalize().into();
673 let leaf_hash = Sha256::digest(root);
674 let transaction_hash = Sha256::digest(leaf_hash);
675 ClearingReference {
676 root,
677 leaves: vec![(leaf_hash.into(), transaction_hash.into())],
678 }
679}
680
681fn random_32() -> [u8; 32] {
682 let mut out = [0u8; 32];
683 OsRng.fill_bytes(&mut out);
684 out
685}
686
687fn random_purse_id() -> PurseId {
688 let mut bytes = [0u8; 4];
689 OsRng.fill_bytes(&mut bytes);
690 u32::from_le_bytes(bytes)
691}
692
693fn random_nonce() -> [u8; NONCE_LEN] {
694 let mut out = [0u8; NONCE_LEN];
695 OsRng.fill_bytes(&mut out);
696 out
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn purse_receivable_cheque_deposit_flow() {
705 let runtime = CoinPaymentRuntime::new(1_700_000_000_000, 500);
706 let capability = runtime.capability();
707 assert_eq!(capability.profile, CoinPaymentProfile::Staged);
708 assert!(capability.supports_statement_store_channel);
709 assert!(capability.supports_refund);
710 assert!(!capability.supports_finality);
711
712 let merchant = runtime.create_purse("terminal", "Terminal");
713 let receivable = runtime.create_receivable(merchant).unwrap();
714 let handoff = runtime.listen_for(receivable).unwrap();
715 assert!(matches!(handoff, TransmissionChannel::Standard { .. }));
716
717 let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 125).unwrap();
718 assert_eq!(runtime.query_purse(MAIN_PURSE).unwrap().balance, 375);
719
720 let statuses = runtime.deposit(cheque).unwrap();
721 assert!(matches!(
722 statuses.last(),
723 Some(Status::Done { cleared: 125, .. })
724 ));
725 assert_eq!(runtime.query_purse(merchant).unwrap().balance, 125);
726 }
727
728 #[test]
729 fn state_snapshot_restores_purses_and_receivable_secret() {
730 let runtime = CoinPaymentRuntime::new(1, 100);
731 let merchant = runtime.create_purse("terminal", "Terminal");
732 let receivable = runtime.create_receivable(merchant).unwrap();
733 let snapshot = runtime.export_state_json().unwrap();
734 let restored = CoinPaymentRuntime::from_state_json(snapshot).unwrap();
735
736 let payer = CoinPaymentRuntime::new(1, 50);
737 let cheque = payer.create_cheque(MAIN_PURSE, receivable, 25).unwrap();
738 restored.deposit(cheque).unwrap();
739
740 assert_eq!(restored.query_purse(merchant).unwrap().balance, 25);
741 }
742
743 #[test]
744 fn state_snapshot_preserves_deposited_cheque_replay_guard() {
745 let runtime = CoinPaymentRuntime::new(1, 100);
746 let merchant = runtime.create_purse("terminal", "Terminal");
747 let receivable = runtime.create_receivable(merchant).unwrap();
748 let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 25).unwrap();
749 runtime.deposit(cheque.clone()).unwrap();
750 let snapshot = runtime.export_state_json().unwrap();
751 let restored = CoinPaymentRuntime::from_state_json(snapshot).unwrap();
752
753 let statuses = restored.deposit(cheque).unwrap();
754 assert!(matches!(
755 statuses.last(),
756 Some(Status::Failed { error, .. }) if error == CoinPaymentErrCode::SnipedCoins.as_str()
757 ));
758 assert_eq!(restored.query_purse(merchant).unwrap().balance, 25);
759 }
760
761 #[test]
762 fn cannot_deposit_cheque_without_receivable_secret() {
763 let sender = CoinPaymentRuntime::new(1, 100);
764 let receiver = CoinPaymentRuntime::new(1, 0);
765 let unknown_receivable = [7u8; 32];
766 let cheque = sender
767 .create_cheque(MAIN_PURSE, unknown_receivable, 10)
768 .unwrap();
769
770 let err = receiver.deposit(cheque).unwrap_err();
771 assert_eq!(err.code, CoinPaymentErrCode::ReceivableNotFound);
772 }
773
774 #[test]
775 fn invoice_payment_uses_main_purse() {
776 let merchant_host = CoinPaymentRuntime::new(1, 0);
777 let payer_host = CoinPaymentRuntime::new(1, 50);
778 let purse = merchant_host.create_purse("terminal", "Terminal");
779 let receivable = merchant_host.create_receivable(purse).unwrap();
780 let invoice = merchant_host.invoice_for(receivable, 20).unwrap();
781
782 let cheque = payer_host.pay_invoice(&invoice).unwrap();
783 merchant_host.receive_cheque(cheque.clone()).unwrap();
784 assert_eq!(
785 merchant_host.pending_cheque(receivable),
786 Some(cheque.clone())
787 );
788 merchant_host.deposit(cheque).unwrap();
789
790 assert_eq!(payer_host.query_purse(MAIN_PURSE).unwrap().balance, 30);
791 assert_eq!(merchant_host.query_purse(purse).unwrap().balance, 20);
792 }
793
794 #[test]
795 fn pay_invoice_rejects_unsupported_version_before_debit() {
796 let payer_host = CoinPaymentRuntime::new(1, 50);
797 let invoice = Invoice {
798 version: 1,
799 handoff: TransmissionChannel::Standard {
800 sss_topic: [3u8; 32],
801 },
802 receiver: [4u8; 32],
803 amount: 20,
804 };
805
806 let err = payer_host.pay_invoice(&invoice).unwrap_err();
807 assert_eq!(err.code, CoinPaymentErrCode::BadCoins);
808 assert_eq!(payer_host.query_purse(MAIN_PURSE).unwrap().balance, 50);
809 }
810
811 #[test]
812 fn duplicate_deposit_fails_without_second_credit() {
813 let runtime = CoinPaymentRuntime::new(1, 100);
814 let merchant = runtime.create_purse("terminal", "Terminal");
815 let receivable = runtime.create_receivable(merchant).unwrap();
816 let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 40).unwrap();
817
818 runtime.deposit(cheque.clone()).unwrap();
819 let statuses = runtime.deposit(cheque).unwrap();
820
821 assert!(matches!(
822 statuses.last(),
823 Some(Status::Failed { error, .. }) if error == CoinPaymentErrCode::SnipedCoins.as_str()
824 ));
825 assert_eq!(runtime.query_purse(merchant).unwrap().balance, 40);
826 }
827
828 #[test]
829 fn deposit_rejects_balance_overflow() {
830 let receiver = CoinPaymentRuntime::new(1, Balance::MAX);
831 let receivable = receiver.create_receivable(MAIN_PURSE).unwrap();
832 let payer = CoinPaymentRuntime::new(1, 50);
833 let cheque = payer.create_cheque(MAIN_PURSE, receivable, 1).unwrap();
834
835 let err = receiver.deposit(cheque).unwrap_err();
836 assert_eq!(err.code, CoinPaymentErrCode::Internal);
837 assert_eq!(
838 receiver.query_purse(MAIN_PURSE).unwrap().balance,
839 Balance::MAX
840 );
841 }
842
843 #[test]
844 fn refund_moves_deposited_amount_back_to_main_purse() {
845 let runtime = CoinPaymentRuntime::new(1, 100);
846 let merchant = runtime.create_purse("terminal", "Terminal");
847 let receivable = runtime.create_receivable(merchant).unwrap();
848 let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 40).unwrap();
849 runtime.deposit(cheque).unwrap();
850 runtime.refund(receivable).unwrap();
851
852 assert_eq!(runtime.query_purse(merchant).unwrap().balance, 0);
853 assert_eq!(runtime.query_purse(MAIN_PURSE).unwrap().balance, 100);
854 }
855
856 #[test]
857 fn refund_to_remote_return_hint_debits_receiver_purse() {
858 let merchant_host = CoinPaymentRuntime::new(1, 0);
859 let payer_host = CoinPaymentRuntime::new(1, 100);
860 let merchant = merchant_host.create_purse("terminal", "Terminal");
861 let receivable = merchant_host.create_receivable(merchant).unwrap();
862 let cheque = payer_host
863 .create_cheque(MAIN_PURSE, receivable, 40)
864 .unwrap();
865 merchant_host.deposit(cheque).unwrap();
866
867 let statuses = merchant_host.refund(receivable).unwrap();
868
869 assert!(matches!(
870 statuses.last(),
871 Some(Status::Done { cleared: 40, .. })
872 ));
873 assert_eq!(merchant_host.query_purse(merchant).unwrap().balance, 0);
874 assert_eq!(payer_host.query_purse(MAIN_PURSE).unwrap().balance, 60);
875 }
876}