onionwire/tests/pay.rs

198 lines
6.1 KiB
Rust
Raw Permalink Normal View History

//! M8: signed Monero invoice/receipt frames + store + slash parse.
use ed25519_dalek::SigningKey;
use onionwire::pay::{self};
use onionwire::tui::{SlashCmd, WipeKind, parse_cmd, parse_slash};
use onionwire::{PaymentWrite, Store};
use rand::rngs::OsRng;
fn store() -> (tempfile::TempDir, Store) {
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open_at_with_passphrase(dir.path(), "onionwire-test").expect("open");
(dir, store)
}
fn addr_std() -> String {
format!("4{}", "A".repeat(94))
}
fn addr_sub() -> String {
format!("8{}", "B".repeat(94))
}
fn addr_integrated() -> String {
format!("4{}", "C".repeat(105))
}
fn payw<'a>(
dir: &'a str,
kind: &'a str,
amount: &'a str,
address: &'a str,
memo: &'a str,
txid: Option<&'a str>,
verified: bool,
) -> PaymentWrite<'a> {
PaymentWrite {
dir,
kind,
amount_atomic: amount,
address,
memo,
txid,
verified,
}
}
#[test]
fn valid_and_invalid_xmr_addresses() {
assert!(pay::check_address(&addr_std()).is_ok());
assert!(pay::check_address(&addr_sub()).is_ok());
assert!(pay::check_address(&addr_integrated()).is_ok());
assert!(pay::check_address(&format!("4{}", "A".repeat(93))).is_err());
assert!(pay::check_address(&format!("8{}", "B".repeat(95))).is_err());
assert!(pay::check_address(&format!("5{}", "A".repeat(94))).is_err());
assert!(pay::check_address("").is_err());
assert!(pay::check_address("not-an-address").is_err());
}
#[test]
fn atomic_display_trims_trailing_zeros() {
assert_eq!(pay::atomic_to_xmr_str("1000000000000"), "1");
assert_eq!(pay::atomic_to_xmr_str("120000000000"), "0.12");
assert_eq!(pay::atomic_to_xmr_str("1"), "0.000000000001");
}
#[test]
fn invoice_sign_verify_and_wrong_key() {
let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().to_bytes();
let addr = addr_std();
let inv = pay::sign_invoice(&sk.to_bytes(), "120000000000", &addr, "coffee", 42).unwrap();
assert!(pay::verify_invoice(&pk, &inv));
let other = SigningKey::generate(&mut OsRng);
assert!(!pay::verify_invoice(
&other.verifying_key().to_bytes(),
&inv
));
let bytes = pay::encode_invoice(&inv);
let parsed = pay::decode_invoice(&bytes).expect("invoice frame");
assert_eq!(parsed.amount_atomic, "120000000000");
assert_eq!(parsed.address, addr);
assert_eq!(parsed.memo, "coffee");
assert_eq!(parsed.ts, 42);
assert_eq!(parsed.sig, inv.sig);
}
#[test]
fn receipt_sign_verify_roundtrip() {
let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().to_bytes();
let addr = addr_sub();
let rcp = pay::sign_receipt(&sk.to_bytes(), "deadbeef", "1", &addr, 99).unwrap();
assert!(pay::verify_receipt(&pk, &rcp));
let bytes = pay::encode_receipt(&rcp);
let parsed = pay::decode_receipt(&bytes).expect("receipt frame");
assert_eq!(parsed.txid, "deadbeef");
assert_eq!(parsed.amount_atomic, "1");
assert_eq!(parsed.address, addr);
assert_eq!(parsed.ts, 99);
}
#[test]
fn chat_is_not_invoice_or_receipt() {
assert!(pay::decode_invoice(b"hello wire").is_none());
assert!(pay::decode_receipt(b"hello wire").is_none());
}
#[test]
fn zero_amount_rejected() {
let sk = [7u8; 32];
let addr = addr_std();
assert!(pay::sign_invoice(&sk, "0", &addr, "", 1).is_err());
assert!(pay::sign_receipt(&sk, "tx", "0", &addr, 1).is_err());
}
#[test]
fn store_insert_invoice_for_friend_unknown_fails() {
let (_dir, store) = store();
let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().to_bytes();
let addr = addr_std();
assert!(
store
.insert_payment(&pk, payw("out", "invoice", "1", &addr, "m", None, false))
.is_err()
);
store.upsert_friend(&pk, "a.onion", None).unwrap();
let id = store
.insert_payment(&pk, payw("out", "invoice", "1", &addr, "m", None, false))
.unwrap();
assert!(id > 0);
let rows = store.list_payments(&pk).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].kind, "invoice");
assert_eq!(rows[0].dir, "out");
assert_eq!(rows[0].amount_atomic, "1");
assert!(!rows[0].verified);
}
#[test]
fn incoming_receipt_is_not_verified() {
let (_dir, store) = store();
let pk = [9u8; 32];
store.upsert_friend(&pk, "b.onion", None).unwrap();
let addr = addr_sub();
let id = store
.insert_payment(
&pk,
payw("in", "receipt", "5", &addr, "", Some("txid1"), false),
)
.unwrap();
let rows = store.list_payments(&pk).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].kind, "receipt");
assert_eq!(rows[0].txid.as_deref(), Some("txid1"));
assert!(!rows[0].verified);
assert!(store.mark_verified(id).unwrap());
let rows = store.list_payments(&pk).unwrap();
assert!(rows[0].verified);
}
#[test]
fn invoice_chat_line_never_raw_prefix() {
let line = pay::invoice_chat_line("120000000000", "coffee");
assert_eq!(line, "[invoice] 0.12 XMR — coffee");
assert!(!line.contains("inv "));
let bare = pay::invoice_chat_line("120000000000", "");
assert_eq!(bare, "[invoice] 0.12 XMR");
let r = pay::receipt_chat_line("120000000000", false);
assert_eq!(r, "[receipt] 0.12 XMR — unverified");
let v = pay::receipt_chat_line("120000000000", true);
assert_eq!(v, "[receipt] 0.12 XMR");
assert!(!v.contains("rcp "));
}
#[test]
fn slash_pay_tip_and_wipe_still_parse() {
assert_eq!(
parse_cmd("/pay 0.12 coffee please"),
Some(SlashCmd::Pay {
atomic: "120000000000".into(),
memo: "coffee please".into(),
})
);
assert_eq!(
parse_cmd(" /tip 1 "),
Some(SlashCmd::Tip {
atomic: "1000000000000".into(),
memo: String::new(),
})
);
assert_eq!(parse_cmd("/pay"), None);
assert_eq!(parse_cmd("/pay 0"), None);
assert_eq!(parse_cmd("/wipe"), Some(SlashCmd::Wipe(WipeKind::Messages)));
assert_eq!(parse_slash("/wipe-all"), Some(WipeKind::All));
assert_eq!(parse_slash("/pay 0.12"), None);
}