mod error;
mod types;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
use rand_core::{OsRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
pub use error::{CoinPaymentErrCode, CoinPaymentError, Result};
pub use types::*;
const CHEQUE_VERSION: u8 = 0;
const ENCRYPTED_CHEQUE_VERSION: u8 = 0;
const NONCE_LEN: usize = 12;
const PUBLIC_KEY_LEN: usize = 32;
#[derive(Clone)]
pub struct CoinPaymentRuntime {
state: Arc<Mutex<State>>,
}
struct State {
now_ms: Timestamp,
next_purse_id: PurseId,
purses: HashMap<PurseId, PurseRecord>,
receivables: HashMap<Receivable, ReceivableRecord>,
pending_cheques: HashMap<Receivable, Cheque>,
deposited_by_receivable: HashMap<Receivable, Balance>,
deposited_cheques: HashSet<[u8; 32]>,
return_hints: HashMap<Receivable, Receivable>,
}
#[derive(Debug, Serialize, Deserialize)]
struct StateSnapshot {
version: u8,
now_ms: Timestamp,
next_purse_id: PurseId,
purses: Vec<SnapshotPurse>,
receivables: Vec<SnapshotReceivable>,
pending_cheques: Vec<Cheque>,
deposited_by_receivable: Vec<SnapshotDepositedBalance>,
#[serde(default)]
deposited_cheques: Vec<[u8; 32]>,
#[serde(default)]
return_hints: Vec<SnapshotReturnHint>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotPurse {
id: PurseId,
info: PurseInfo,
}
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotReceivable {
receivable: Receivable,
into: PurseId,
secret: [u8; 32],
channel: Option<TransmissionChannel>,
}
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotDepositedBalance {
receivable: Receivable,
balance: Balance,
}
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotReturnHint {
receivable: Receivable,
return_hint: Receivable,
}
#[derive(Debug, Clone)]
struct PurseRecord {
info: PurseInfo,
}
#[derive(Clone)]
struct ReceivableRecord {
into: PurseId,
secret: StaticSecret,
channel: Option<TransmissionChannel>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ChequePayload {
version: u8,
receivable: Receivable,
amount: Balance,
return_hint: Option<Receivable>,
}
impl CoinPaymentRuntime {
pub fn capability(&self) -> CoinPaymentCapability {
CoinPaymentCapability {
version: 0,
profile: CoinPaymentProfile::Staged,
supports_statement_store_channel: true,
supports_refund: true,
supports_finality: false,
}
}
pub fn new(now_ms: Timestamp, main_balance: Balance) -> Self {
let mut purses = HashMap::new();
purses.insert(
MAIN_PURSE,
PurseRecord {
info: PurseInfo {
name: "Main purse".to_string(),
created: now_ms,
creator: "user-agent".to_string(),
balance: main_balance,
},
},
);
Self {
state: Arc::new(Mutex::new(State {
now_ms,
next_purse_id: 1,
purses,
receivables: HashMap::new(),
pending_cheques: HashMap::new(),
deposited_by_receivable: HashMap::new(),
deposited_cheques: HashSet::new(),
return_hints: HashMap::new(),
})),
}
}
pub fn from_state_json(state_json: impl AsRef<str>) -> Result<Self> {
let snapshot: StateSnapshot =
serde_json::from_str(state_json.as_ref()).map_err(|error| {
CoinPaymentError::internal(format!("could not decode coinpayment state: {error}"))
})?;
State::from_snapshot(snapshot).map(|state| Self {
state: Arc::new(Mutex::new(state)),
})
}
pub fn export_state_json(&self) -> Result<String> {
let state = self.lock_state();
serde_json::to_string(&state.to_snapshot()).map_err(|error| {
CoinPaymentError::internal(format!("could not encode coinpayment state: {error}"))
})
}
pub fn create_purse(
&self,
product_id: impl Into<ProductId>,
name: impl Into<String>,
) -> PurseId {
let mut state = self.lock_state();
let purse = state.allocate_purse_id();
let record = PurseRecord {
info: PurseInfo {
name: name.into(),
created: state.now_ms,
creator: product_id.into(),
balance: 0,
},
};
state.purses.insert(purse, record);
purse
}
pub fn query_purse(&self, purse: PurseId) -> Result<PurseInfo> {
let state = self.lock_state();
Ok(state.purse(purse)?.info.clone())
}
pub fn rebalance_purse(
&self,
from: PurseId,
to: PurseId,
amount: Balance,
) -> Result<Vec<Status>> {
let mut state = self.lock_state();
state.transfer_balance(from, to, amount)?;
Ok(done_statuses(amount))
}
pub fn delete_purse(&self, target: PurseId, drain_into: PurseId) -> Result<Vec<Status>> {
if target == MAIN_PURSE {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::Denied,
"MAIN_PURSE cannot be deleted",
));
}
let mut state = self.lock_state();
let amount = state.purse(target)?.info.balance;
state.transfer_balance(target, drain_into, amount)?;
state.purses.remove(&target);
Ok(done_statuses(amount))
}
pub fn create_receivable(&self, into: PurseId) -> Result<Receivable> {
let mut state = self.lock_state();
state.purse(into)?;
let secret = StaticSecret::random_from_rng(OsRng);
let receivable = PublicKey::from(&secret).to_bytes();
state.receivables.insert(
receivable,
ReceivableRecord {
into,
secret,
channel: None,
},
);
Ok(receivable)
}
pub fn listen_for(&self, receivable: Receivable) -> Result<TransmissionChannel> {
let mut state = self.lock_state();
let record = state.receivable_mut(&receivable)?;
if let Some(channel) = &record.channel {
return Ok(channel.clone());
}
let channel = TransmissionChannel::Standard {
sss_topic: random_32(),
};
record.channel = Some(channel.clone());
Ok(channel)
}
pub fn invoice_for(&self, receiver: Receivable, amount: Balance) -> Result<Invoice> {
let handoff = self.listen_for(receiver)?;
Ok(Invoice {
version: CHEQUE_VERSION,
handoff,
receiver,
amount,
})
}
pub fn create_cheque(&self, from: PurseId, to: Receivable, amount: Balance) -> Result<Cheque> {
let mut state = self.lock_state();
if state.purse(from)?.info.balance < amount {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BalanceLow,
"source purse balance is too low",
));
}
let return_secret = StaticSecret::random_from_rng(OsRng);
let return_hint = PublicKey::from(&return_secret).to_bytes();
state.receivables.insert(
return_hint,
ReceivableRecord {
into: from,
secret: return_secret,
channel: None,
},
);
state.purse_mut(from)?.info.balance -= amount;
drop(state);
encrypt_cheque_payload(to, amount, Some(return_hint))
}
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<Cheque> {
if invoice.version != CHEQUE_VERSION {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BadCoins,
"unsupported invoice version",
));
}
match &invoice.handoff {
TransmissionChannel::Standard { .. } => {
self.create_cheque(MAIN_PURSE, invoice.receiver, invoice.amount)
}
}
}
pub fn receive_cheque(&self, cheque: Cheque) -> Result<()> {
if cheque.version != CHEQUE_VERSION {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BadCoins,
"unsupported cheque version",
));
}
let mut state = self.lock_state();
state.pending_cheques.insert(cheque.id, cheque);
Ok(())
}
pub fn pending_cheque(&self, receivable: Receivable) -> Option<Cheque> {
self.lock_state().pending_cheques.get(&receivable).cloned()
}
pub fn deposit(&self, cheque: Cheque) -> Result<Vec<Status>> {
if cheque.version != CHEQUE_VERSION {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BadCoins,
"unsupported cheque version",
));
}
let mut state = self.lock_state();
let receivable = state.receivable(&cheque.id)?.clone();
let payload = decrypt_cheque_payload(&receivable.secret, &cheque)?;
if payload.receivable != cheque.id || payload.amount != cheque.amount {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BadCoins,
"cheque payload does not match public cheque fields",
));
}
let cheque_hash = cheque_id(&cheque);
if state.deposited_cheques.contains(&cheque_hash) {
return Ok(vec![failed_status(
CoinPaymentErrCode::SnipedCoins,
state
.deposited_by_receivable
.get(&cheque.id)
.copied()
.unwrap_or(0),
)]);
}
let purse = state.purse_mut(receivable.into)?;
purse.info.balance = purse
.info
.balance
.checked_add(payload.amount)
.ok_or_else(|| {
CoinPaymentError::new(CoinPaymentErrCode::Internal, "purse balance overflow")
})?;
let deposited = state.deposited_by_receivable.entry(cheque.id).or_insert(0);
*deposited = deposited.checked_add(payload.amount).ok_or_else(|| {
CoinPaymentError::new(
CoinPaymentErrCode::Internal,
"deposited receivable balance overflow",
)
})?;
if let Some(return_hint) = payload.return_hint {
state.return_hints.insert(cheque.id, return_hint);
}
state.deposited_cheques.insert(cheque_hash);
state.pending_cheques.remove(&cheque.id);
Ok(done_statuses(payload.amount))
}
pub fn refund(&self, receivable: Receivable) -> Result<Vec<Status>> {
let mut state = self.lock_state();
let record = state.receivable(&receivable)?.clone();
let amount = *state.deposited_by_receivable.get(&receivable).unwrap_or(&0);
if amount == 0 {
return Ok(vec![failed_status(
CoinPaymentErrCode::ReceivableNotFound,
0,
)]);
}
let Some(return_hint) = state.return_hints.get(&receivable).copied() else {
return Ok(vec![failed_status(
CoinPaymentErrCode::UnsupportedChannel,
0,
)]);
};
if let Ok(return_record) = state.receivable(&return_hint).cloned() {
state.transfer_balance(record.into, return_record.into, amount)?;
} else {
let source = state.purse_mut(record.into)?;
if source.info.balance < amount {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BalanceLow,
"source purse balance is too low",
));
}
source.info.balance -= amount;
}
state.deposited_by_receivable.insert(receivable, 0);
Ok(done_statuses(amount))
}
fn lock_state(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl State {
fn from_snapshot(snapshot: StateSnapshot) -> Result<Self> {
if snapshot.version != 0 {
return Err(CoinPaymentError::internal(
"unsupported coinpayment state version",
));
}
let mut purses = HashMap::new();
for purse in snapshot.purses {
purses.insert(purse.id, PurseRecord { info: purse.info });
}
if !purses.contains_key(&MAIN_PURSE) {
return Err(CoinPaymentError::internal(
"coinpayment state is missing MAIN_PURSE",
));
}
let mut receivables = HashMap::new();
for receivable in snapshot.receivables {
let secret = StaticSecret::from(receivable.secret);
let public = PublicKey::from(&secret).to_bytes();
if public != receivable.receivable {
return Err(CoinPaymentError::internal(
"coinpayment state receivable secret does not match public key",
));
}
if !purses.contains_key(&receivable.into) {
return Err(CoinPaymentError::internal(
"coinpayment state receivable targets a missing purse",
));
}
receivables.insert(
receivable.receivable,
ReceivableRecord {
into: receivable.into,
secret,
channel: receivable.channel,
},
);
}
let pending_cheques = snapshot
.pending_cheques
.into_iter()
.map(|cheque| (cheque.id, cheque))
.collect();
let deposited_by_receivable = snapshot
.deposited_by_receivable
.into_iter()
.map(|entry| (entry.receivable, entry.balance))
.collect();
Ok(Self {
now_ms: snapshot.now_ms,
next_purse_id: snapshot.next_purse_id.max(1),
purses,
receivables,
pending_cheques,
deposited_by_receivable,
deposited_cheques: snapshot.deposited_cheques.into_iter().collect(),
return_hints: snapshot
.return_hints
.into_iter()
.map(|entry| (entry.receivable, entry.return_hint))
.collect(),
})
}
fn to_snapshot(&self) -> StateSnapshot {
StateSnapshot {
version: 0,
now_ms: self.now_ms,
next_purse_id: self.next_purse_id,
purses: self
.purses
.iter()
.map(|(id, record)| SnapshotPurse {
id: *id,
info: record.info.clone(),
})
.collect(),
receivables: self
.receivables
.iter()
.map(|(receivable, record)| SnapshotReceivable {
receivable: *receivable,
into: record.into,
secret: record.secret.to_bytes(),
channel: record.channel.clone(),
})
.collect(),
pending_cheques: self.pending_cheques.values().cloned().collect(),
deposited_by_receivable: self
.deposited_by_receivable
.iter()
.map(|(receivable, balance)| SnapshotDepositedBalance {
receivable: *receivable,
balance: *balance,
})
.collect(),
deposited_cheques: self.deposited_cheques.iter().copied().collect(),
return_hints: self
.return_hints
.iter()
.map(|(receivable, return_hint)| SnapshotReturnHint {
receivable: *receivable,
return_hint: *return_hint,
})
.collect(),
}
}
fn allocate_purse_id(&mut self) -> PurseId {
loop {
let id = random_purse_id();
if id != MAIN_PURSE && !self.purses.contains_key(&id) {
return id;
}
}
}
fn purse(&self, purse: PurseId) -> Result<&PurseRecord> {
self.purses.get(&purse).ok_or_else(|| {
CoinPaymentError::new(CoinPaymentErrCode::PurseNotFound, "purse was not found")
})
}
fn purse_mut(&mut self, purse: PurseId) -> Result<&mut PurseRecord> {
self.purses.get_mut(&purse).ok_or_else(|| {
CoinPaymentError::new(CoinPaymentErrCode::PurseNotFound, "purse was not found")
})
}
fn receivable(&self, receivable: &Receivable) -> Result<&ReceivableRecord> {
self.receivables.get(receivable).ok_or_else(|| {
CoinPaymentError::new(
CoinPaymentErrCode::ReceivableNotFound,
"receivable was not found",
)
})
}
fn receivable_mut(&mut self, receivable: &Receivable) -> Result<&mut ReceivableRecord> {
self.receivables.get_mut(receivable).ok_or_else(|| {
CoinPaymentError::new(
CoinPaymentErrCode::ReceivableNotFound,
"receivable was not found",
)
})
}
fn transfer_balance(&mut self, from: PurseId, to: PurseId, amount: Balance) -> Result<()> {
if from == to || amount == 0 {
self.purse(from)?;
self.purse(to)?;
return Ok(());
}
let source_balance = self.purse(from)?.info.balance;
if source_balance < amount {
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BalanceLow,
"source purse balance is too low",
));
}
self.purse_mut(from)?.info.balance -= amount;
let target = self.purse_mut(to)?;
target.info.balance = target.info.balance.checked_add(amount).ok_or_else(|| {
CoinPaymentError::new(CoinPaymentErrCode::Internal, "purse balance overflow")
})?;
Ok(())
}
}
fn encrypt_cheque_payload(
receivable: Receivable,
amount: Balance,
return_hint: Option<Receivable>,
) -> Result<Cheque> {
let secret = StaticSecret::random_from_rng(OsRng);
let ephemeral_public = PublicKey::from(&secret);
let receiver_public = PublicKey::from(receivable);
let shared = secret.diffie_hellman(&receiver_public);
let key = cheque_key(shared.as_bytes(), &receivable, ephemeral_public.as_bytes());
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|_| CoinPaymentError::internal("could not initialize cheque encryption cipher"))?;
let nonce_bytes = random_nonce();
let nonce = Nonce::from_slice(&nonce_bytes);
let payload = ChequePayload {
version: CHEQUE_VERSION,
receivable,
amount,
return_hint,
};
let payload_bytes = Zeroizing::new(serde_json::to_vec(&payload).map_err(|error| {
CoinPaymentError::internal(format!("could not encode cheque payload: {error}"))
})?);
let ciphertext = cipher
.encrypt(nonce, payload_bytes.as_ref())
.map_err(|_| CoinPaymentError::internal("could not encrypt cheque payload"))?;
let mut encrypted_secrets =
Vec::with_capacity(1 + PUBLIC_KEY_LEN + NONCE_LEN + ciphertext.len());
encrypted_secrets.push(ENCRYPTED_CHEQUE_VERSION);
encrypted_secrets.extend_from_slice(ephemeral_public.as_bytes());
encrypted_secrets.extend_from_slice(&nonce_bytes);
encrypted_secrets.extend_from_slice(&ciphertext);
Ok(Cheque {
version: CHEQUE_VERSION,
id: receivable,
amount,
encrypted_secrets,
})
}
fn decrypt_cheque_payload(secret: &StaticSecret, cheque: &Cheque) -> Result<ChequePayload> {
let encrypted = &cheque.encrypted_secrets;
if encrypted.len() <= 1 + PUBLIC_KEY_LEN + NONCE_LEN || encrypted[0] != ENCRYPTED_CHEQUE_VERSION
{
return Err(CoinPaymentError::new(
CoinPaymentErrCode::BadCoins,
"invalid cheque encrypted payload",
));
}
let mut public = [0u8; PUBLIC_KEY_LEN];
public.copy_from_slice(&encrypted[1..1 + PUBLIC_KEY_LEN]);
let nonce_start = 1 + PUBLIC_KEY_LEN;
let nonce_end = nonce_start + NONCE_LEN;
let nonce = Nonce::from_slice(&encrypted[nonce_start..nonce_end]);
let ciphertext = &encrypted[nonce_end..];
let ephemeral_public = PublicKey::from(public);
let shared = secret.diffie_hellman(&ephemeral_public);
let key = cheque_key(shared.as_bytes(), &cheque.id, &public);
let cipher = Aes256Gcm::new_from_slice(&key)
.map_err(|_| CoinPaymentError::internal("could not initialize cheque decryption cipher"))?;
let payload = cipher.decrypt(nonce, ciphertext).map_err(|_| {
CoinPaymentError::new(CoinPaymentErrCode::BadCoins, "could not decrypt cheque")
})?;
serde_json::from_slice(&payload).map_err(|error| {
CoinPaymentError::new(
CoinPaymentErrCode::BadCoins,
format!("could not decode cheque payload: {error}"),
)
})
}
fn cheque_key(shared: &[u8; 32], receivable: &Receivable, ephemeral_public: &[u8; 32]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"useragent-kit:coinpayment:rfc17:cheque:v0");
hasher.update(shared);
hasher.update(receivable);
hasher.update(ephemeral_public);
hasher.finalize().into()
}
fn cheque_id(cheque: &Cheque) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(b"useragent-kit:coinpayment:rfc17:cheque-id:v0");
hasher.update([cheque.version]);
hasher.update(cheque.id);
hasher.update(cheque.amount.to_le_bytes());
hasher.update(&cheque.encrypted_secrets);
hasher.finalize().into()
}
fn done_statuses(amount: Balance) -> Vec<Status> {
vec![
Status::Clearing {
clearing: amount,
cleared: 0,
},
Status::Done {
cleared: amount,
reference: clearing_reference(amount),
},
]
}
fn failed_status(error: CoinPaymentErrCode, cleared: Balance) -> Status {
Status::Failed {
error: error.as_str().to_string(),
cleared,
reference: clearing_reference(cleared),
}
}
fn clearing_reference(amount: Balance) -> ClearingReference {
let mut root_hasher = Sha256::new();
root_hasher.update(b"useragent-kit:coinpayment:rfc17:clearing");
root_hasher.update(amount.to_le_bytes());
let root: [u8; 32] = root_hasher.finalize().into();
let leaf_hash = Sha256::digest(root);
let transaction_hash = Sha256::digest(leaf_hash);
ClearingReference {
root,
leaves: vec![(leaf_hash.into(), transaction_hash.into())],
}
}
fn random_32() -> [u8; 32] {
let mut out = [0u8; 32];
OsRng.fill_bytes(&mut out);
out
}
fn random_purse_id() -> PurseId {
let mut bytes = [0u8; 4];
OsRng.fill_bytes(&mut bytes);
u32::from_le_bytes(bytes)
}
fn random_nonce() -> [u8; NONCE_LEN] {
let mut out = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn purse_receivable_cheque_deposit_flow() {
let runtime = CoinPaymentRuntime::new(1_700_000_000_000, 500);
let capability = runtime.capability();
assert_eq!(capability.profile, CoinPaymentProfile::Staged);
assert!(capability.supports_statement_store_channel);
assert!(capability.supports_refund);
assert!(!capability.supports_finality);
let merchant = runtime.create_purse("terminal", "Terminal");
let receivable = runtime.create_receivable(merchant).unwrap();
let handoff = runtime.listen_for(receivable).unwrap();
assert!(matches!(handoff, TransmissionChannel::Standard { .. }));
let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 125).unwrap();
assert_eq!(runtime.query_purse(MAIN_PURSE).unwrap().balance, 375);
let statuses = runtime.deposit(cheque).unwrap();
assert!(matches!(
statuses.last(),
Some(Status::Done { cleared: 125, .. })
));
assert_eq!(runtime.query_purse(merchant).unwrap().balance, 125);
}
#[test]
fn state_snapshot_restores_purses_and_receivable_secret() {
let runtime = CoinPaymentRuntime::new(1, 100);
let merchant = runtime.create_purse("terminal", "Terminal");
let receivable = runtime.create_receivable(merchant).unwrap();
let snapshot = runtime.export_state_json().unwrap();
let restored = CoinPaymentRuntime::from_state_json(snapshot).unwrap();
let payer = CoinPaymentRuntime::new(1, 50);
let cheque = payer.create_cheque(MAIN_PURSE, receivable, 25).unwrap();
restored.deposit(cheque).unwrap();
assert_eq!(restored.query_purse(merchant).unwrap().balance, 25);
}
#[test]
fn state_snapshot_preserves_deposited_cheque_replay_guard() {
let runtime = CoinPaymentRuntime::new(1, 100);
let merchant = runtime.create_purse("terminal", "Terminal");
let receivable = runtime.create_receivable(merchant).unwrap();
let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 25).unwrap();
runtime.deposit(cheque.clone()).unwrap();
let snapshot = runtime.export_state_json().unwrap();
let restored = CoinPaymentRuntime::from_state_json(snapshot).unwrap();
let statuses = restored.deposit(cheque).unwrap();
assert!(matches!(
statuses.last(),
Some(Status::Failed { error, .. }) if error == CoinPaymentErrCode::SnipedCoins.as_str()
));
assert_eq!(restored.query_purse(merchant).unwrap().balance, 25);
}
#[test]
fn cannot_deposit_cheque_without_receivable_secret() {
let sender = CoinPaymentRuntime::new(1, 100);
let receiver = CoinPaymentRuntime::new(1, 0);
let unknown_receivable = [7u8; 32];
let cheque = sender
.create_cheque(MAIN_PURSE, unknown_receivable, 10)
.unwrap();
let err = receiver.deposit(cheque).unwrap_err();
assert_eq!(err.code, CoinPaymentErrCode::ReceivableNotFound);
}
#[test]
fn invoice_payment_uses_main_purse() {
let merchant_host = CoinPaymentRuntime::new(1, 0);
let payer_host = CoinPaymentRuntime::new(1, 50);
let purse = merchant_host.create_purse("terminal", "Terminal");
let receivable = merchant_host.create_receivable(purse).unwrap();
let invoice = merchant_host.invoice_for(receivable, 20).unwrap();
let cheque = payer_host.pay_invoice(&invoice).unwrap();
merchant_host.receive_cheque(cheque.clone()).unwrap();
assert_eq!(
merchant_host.pending_cheque(receivable),
Some(cheque.clone())
);
merchant_host.deposit(cheque).unwrap();
assert_eq!(payer_host.query_purse(MAIN_PURSE).unwrap().balance, 30);
assert_eq!(merchant_host.query_purse(purse).unwrap().balance, 20);
}
#[test]
fn pay_invoice_rejects_unsupported_version_before_debit() {
let payer_host = CoinPaymentRuntime::new(1, 50);
let invoice = Invoice {
version: 1,
handoff: TransmissionChannel::Standard {
sss_topic: [3u8; 32],
},
receiver: [4u8; 32],
amount: 20,
};
let err = payer_host.pay_invoice(&invoice).unwrap_err();
assert_eq!(err.code, CoinPaymentErrCode::BadCoins);
assert_eq!(payer_host.query_purse(MAIN_PURSE).unwrap().balance, 50);
}
#[test]
fn duplicate_deposit_fails_without_second_credit() {
let runtime = CoinPaymentRuntime::new(1, 100);
let merchant = runtime.create_purse("terminal", "Terminal");
let receivable = runtime.create_receivable(merchant).unwrap();
let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 40).unwrap();
runtime.deposit(cheque.clone()).unwrap();
let statuses = runtime.deposit(cheque).unwrap();
assert!(matches!(
statuses.last(),
Some(Status::Failed { error, .. }) if error == CoinPaymentErrCode::SnipedCoins.as_str()
));
assert_eq!(runtime.query_purse(merchant).unwrap().balance, 40);
}
#[test]
fn deposit_rejects_balance_overflow() {
let receiver = CoinPaymentRuntime::new(1, Balance::MAX);
let receivable = receiver.create_receivable(MAIN_PURSE).unwrap();
let payer = CoinPaymentRuntime::new(1, 50);
let cheque = payer.create_cheque(MAIN_PURSE, receivable, 1).unwrap();
let err = receiver.deposit(cheque).unwrap_err();
assert_eq!(err.code, CoinPaymentErrCode::Internal);
assert_eq!(
receiver.query_purse(MAIN_PURSE).unwrap().balance,
Balance::MAX
);
}
#[test]
fn refund_moves_deposited_amount_back_to_main_purse() {
let runtime = CoinPaymentRuntime::new(1, 100);
let merchant = runtime.create_purse("terminal", "Terminal");
let receivable = runtime.create_receivable(merchant).unwrap();
let cheque = runtime.create_cheque(MAIN_PURSE, receivable, 40).unwrap();
runtime.deposit(cheque).unwrap();
runtime.refund(receivable).unwrap();
assert_eq!(runtime.query_purse(merchant).unwrap().balance, 0);
assert_eq!(runtime.query_purse(MAIN_PURSE).unwrap().balance, 100);
}
#[test]
fn refund_to_remote_return_hint_debits_receiver_purse() {
let merchant_host = CoinPaymentRuntime::new(1, 0);
let payer_host = CoinPaymentRuntime::new(1, 100);
let merchant = merchant_host.create_purse("terminal", "Terminal");
let receivable = merchant_host.create_receivable(merchant).unwrap();
let cheque = payer_host
.create_cheque(MAIN_PURSE, receivable, 40)
.unwrap();
merchant_host.deposit(cheque).unwrap();
let statuses = merchant_host.refund(receivable).unwrap();
assert!(matches!(
statuses.last(),
Some(Status::Done { cleared: 40, .. })
));
assert_eq!(merchant_host.query_purse(merchant).unwrap().balance, 0);
assert_eq!(payer_host.query_purse(MAIN_PURSE).unwrap().balance, 60);
}
}