use serde::Serialize; use assert_matches::assert_matches; use serde_json::Value; use zcash_primitives::memo::MemoBytes; use crate::note_selection::build_tx_plan; use crate::note_selection::fee::{FeeCalculator, FeeZIP327}; use crate::note_selection::optimize::{outputs_for_change, select_inputs}; use crate::note_selection::ua::decode; use super::types::*; use super::optimize::{allocate_funds, fill, group_orders}; use super::TransactionBuilderError::NotEnoughFunds; macro_rules! utxo { ($id:expr, $q:expr) => { UTXO { amount: $q * 1000, source: Source::Transparent { txid: [0u8; 32], index: $id, }, } }; } macro_rules! sapling { ($id:expr, $q:expr) => { UTXO { amount: $q * 1000, source: Source::Sapling { id_note: $id, diversifier: [0u8; 11], rseed: [0u8; 32], witness: vec![], }, } }; } macro_rules! orchard { ($id:expr, $q:expr) => { UTXO { amount: $q * 1000, source: Source::Orchard { id_note: $id, diversifier: [0u8; 11], rseed: [0u8; 32], rho: [0u8; 32], witness: vec![], }, } }; } macro_rules! order { ($id:expr, $q:expr, $destinations:expr) => { Order { id: $id, amount: $q * 1000, destinations: $destinations, memo: MemoBytes::empty(), } }; } macro_rules! t { ($id: expr, $q:expr) => { order!( $id, $q, [Some(Destination::Transparent([0u8; 20])), None, None] ) }; } macro_rules! s { ($id: expr, $q:expr) => { order!($id, $q, [None, Some(Destination::Sapling([0u8; 43])), None]) }; } macro_rules! o { ($id: expr, $q:expr) => { order!($id, $q, [None, None, Some(Destination::Orchard([0u8; 43]))]) }; } macro_rules! ts { ($id: expr, $q:expr) => { order!( $id, $q, [ Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), None ] ) }; } macro_rules! to { ($id: expr, $q:expr) => { order!( $id, $q, [ Some(Destination::Transparent([0u8; 20])), None, Some(Destination::Orchard([0u8; 43])) ] ) }; } macro_rules! so { ($id: expr, $q:expr) => { order!( $id, $q, [ None, Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43])) ] ) }; } macro_rules! tso { ($id: expr, $q:expr) => { order!( $id, $q, [ Some(Destination::Transparent([0u8; 20])), Some(Destination::Sapling([0u8; 43])), Some(Destination::Orchard([0u8; 43])) ] ) }; } #[test] #[ignore] fn test_select() { env_logger::init(); // Exhaustive test of every combination of T/S/O/S+O recipients // with every combination of assets in sender's account let mut c = 0usize; for t in 0..=10 { for s in 0..=10 { for o in 0..=10 { for so in 0..=10 { for fee in 0..=10 { let amounts = OrderGroupAmounts { t0: t * 10_000, s0: s * 10_000, o0: o * 10_000, x: so * 10_000, fee: fee * 1000, }; for t in 0..=10 { for s in 0..=10 { for o in 0..=10 { let _ = allocate_funds(&amounts, &&PoolAllocation([t * 20_000, s * 20_000, o * 20_000])); c += 1; } } } } } } } } println!("{} tests", c); } #[test] fn test_t2t() { let r = allocate_funds(&OrderGroupAmounts { t0: 100, s0: 0, o0: 0, x: 0, fee: 10 }, &PoolAllocation([150, 0, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 110, s2: 0, o2: 0 }) } #[test] fn test_t2zs() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 100, o0: 0, x: 0, fee: 10 }, &PoolAllocation([150, 0, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 110, s2: 0, o2: 0 }) } #[test] fn test_t2zo() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 100, x: 0, fee: 10 }, &PoolAllocation([150, 0, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 110, s2: 0, o2: 0 }) } #[test] fn test_t2ua() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 0, x: 100, fee: 10 }, &PoolAllocation([150, 0, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 50, o1: 50, t2: 110, s2: 0, o2: 0 }) } #[test] fn test_zs2zs() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 100, o0: 0, x: 0, fee: 10 }, &PoolAllocation([0, 150, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 110, o2: 0 }) } #[test] fn test_zo2zo() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 100, x: 0, fee: 10 }, &PoolAllocation([0, 0, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 0, o2: 110 }) } #[test] fn test_ua2zs() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 100, o0: 0, x: 0, fee: 10 }, &PoolAllocation([0, 150, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 105, o2: 5, }) // net change is (-5, -5) which is better than (-10, 0) } #[test] fn test_ua2zo() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 100, x: 0, fee: 10 }, &PoolAllocation([0, 150, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 5, o2: 105, }) // net change is (-5, -5) which is better than (-10, 0) } #[test] fn test_ua2t() { let r = allocate_funds(&OrderGroupAmounts { t0: 100, s0: 0, o0: 0, x: 0, fee: 10 }, &PoolAllocation([0, 150, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 55, o2: 55, }) // split equally between sapling & orchard } #[test] fn test_zs2t() { let r = allocate_funds(&OrderGroupAmounts { t0: 100, s0: 0, o0: 0, x: 0, fee: 10 }, &PoolAllocation([0, 150, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 110, o2: 0, }) } #[test] fn test_zo2t() { let r = allocate_funds(&OrderGroupAmounts { t0: 100, s0: 0, o0: 0, x: 0, fee: 10 }, &PoolAllocation([0, 0, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 0, o2: 110, }) } #[test] fn test_zo2zs() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 100, o0: 0, x: 0, fee: 10 }, &PoolAllocation([0, 0, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 0, o2: 110, }) } #[test] fn test_zs2zo() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 100, x: 0, fee: 10 }, &PoolAllocation([0, 150, 0])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 0, s2: 110, o2: 0, }) } #[test] fn test_ua2ua() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 0, x: 100, fee: 10 }, &PoolAllocation([0, 150, 150])).unwrap(); assert_eq!(r, FundAllocation { s1: 50, o1: 50, t2: 0, s2: 55, o2: 55, }) } #[test] fn test_tzs2zs() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 100, o0: 0, x: 0, fee: 10 }, &PoolAllocation([150, 10, 10])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 0, t2: 90, // must use t because not enough zs & zo s2: 10, o2: 10, }) } #[test] fn test_tzs2ua() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 0, x: 100, fee: 10 }, &PoolAllocation([150, 10, 10])).unwrap(); assert_eq!(r, FundAllocation { s1: 50, o1: 50, // split equally to minimize net change t2: 90, // must use t because not enough zs & zo s2: 10, o2: 10, }) } #[test] fn test_neg_ua2ua() { let r = allocate_funds(&OrderGroupAmounts { t0: 0, s0: 0, o0: 0, x: 100, fee: 10 }, &PoolAllocation([10, 10, 10])); assert_matches!(r, Err(NotEnoughFunds)) } #[test] fn test_odd_ua2ua() { let r = allocate_funds(&OrderGroupAmounts { t0: 1, s0: 1, o0: 1, x: 1, fee: 1 }, &PoolAllocation([10, 10, 10])).unwrap(); assert_eq!(r, FundAllocation { s1: 0, o1: 1, t2: 0, s2: 2, o2: 3, }) } #[test] fn test_fill() { let _ = env_logger::try_init(); let orders = vec![ t!(1, 10), s!(2, 20), o!(3, 30), ts!(4, 40), to!(5, 50), so!(6, 60), tso!(7, 70), ]; let (groups, amounts) = group_orders(&orders, 0).unwrap(); assert_eq!(amounts, OrderGroupAmounts { t0: 10_000, s0: 60_000, o0: 80_000, x: 130_000, fee: 0 }); let allocation = allocate_funds(&amounts, &PoolAllocation([200_000, 200_000, 200_000])).unwrap(); let fills = fill(&orders, &groups, &amounts, &allocation).unwrap(); log::info!("{:?}", allocation); log::info!("{:?}", fills); assert_eq!(fills[5].amount + fills[6].amount, 60_000); assert_eq!(fills[7].amount + fills[8].amount, 70_000); assert_eq!(fills[1].amount + fills[3].amount + fills[5].amount + fills[7].amount, fills[2].amount + fills[4].amount + fills[6].amount + fills[8].amount); } #[test] fn test_select_utxo() { let _ = env_logger::try_init(); let allocation = FundAllocation { s1: 75000, o1: 55000, t2: 0, s2: 140000, o2: 140000 }; let mut utxos = vec![]; for i in 0..30 { if i < 10 { utxos.push(utxo!(i, 25)); } else if i < 20 { utxos.push(sapling!(i, 25)); } else { utxos.push(orchard!(i, 25)); } } let (_inputs, change) = select_inputs(&utxos, &allocation).unwrap(); assert_eq!(change.0, [0, 10000, 10000]); } const CHANGE_ADDRESS: &str = "u1pncsxa8jt7aq37r8uvhjrgt7sv8a665hdw44rqa28cd9t6qqmktzwktw772nlle6skkkxwmtzxaan3slntqev03g70tzpky3c58hfgvfjkcky255cwqgfuzdjcktfl7pjalt5sl33se75pmga09etn9dplr98eq2g8cgmvgvx6jx2a2xhy39x96c6rumvlyt35whml87r064qdzw30e"; #[test] fn test_change_fills() { let _ = env_logger::try_init(); let destinations = decode(CHANGE_ADDRESS).unwrap(); let outputs = outputs_for_change(&destinations, &&PoolAllocation([0, 10000, 10000])).unwrap(); log::info!("{:?}", outputs); } #[test] fn test_fees() { let _ = env_logger::try_init(); let utxos = utxos(); let orders = vec![ t!(1, 10), s!(2, 20), o!(3, 30), ts!(4, 40), to!(5, 50), so!(6, 60), tso!(7, 70), ]; let (groups, amounts) = group_orders(&orders, 0).unwrap(); let allocation = allocate_funds(&amounts, &PoolAllocation([200_000, 200_000, 200_000])).unwrap(); let fills = fill(&orders, &groups, &amounts, &allocation).unwrap(); let fees = FeeZIP327::calculate_fee( &utxos, &fills); assert_eq!(fees, 150_000); } #[test] fn test_tx_plan() { let _ = env_logger::try_init(); let utxos = utxos(); let orders = vec![ t!(1, 10), s!(2, 20), o!(3, 30), ts!(4, 40), to!(5, 50), so!(6, 60), tso!(7, 70), ]; let tx_plan = build_tx_plan::("", 0, &utxos, &orders, &TransactionBuilderConfig { change_address: CHANGE_ADDRESS.to_string() }).unwrap(); let simple_plan: SimpleTxPlan = tx_plan.into(); let plan = serde_json::to_string(&simple_plan).unwrap(); log::info!("{}", plan); let tx_plan_json = serde_json::to_value(&simple_plan).unwrap(); let expected: Value = serde_json::from_str(r#"{ "inputs": [{ "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 1, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }, { "pool": 2, "amount": 25000 }], "outputs": [{ "pool": 0, "amount": 10000 }, { "pool": 1, "amount": 20000 }, { "pool": 2, "amount": 30000 }, { "pool": 1, "amount": 40000 }, { "pool": 2, "amount": 50000 }, { "pool": 1, "amount": 34615 }, { "pool": 2, "amount": 25385 }, { "pool": 1, "amount": 40385 }, { "pool": 2, "amount": 29615 }, { "pool": 1, "amount": 17500 }, { "pool": 2, "amount": 17500 }], "fee": 85000 }"#).unwrap(); assert_eq!(tx_plan_json, expected); } #[derive(Serialize)] struct SimpleTxPlan { inputs: Vec, outputs: Vec, fee: u64, } #[derive(Serialize)] struct SimpleTxIO { pool: u8, amount: u64, } impl From for SimpleTxPlan { fn from(p: TransactionPlan) -> Self { SimpleTxPlan { inputs: p.spends.iter().map(|utxo| SimpleTxIO { pool: utxo.source.pool() as u8, amount: utxo.amount, }).collect(), outputs: p.outputs.iter().map(|utxo| SimpleTxIO { pool: utxo.destination.pool() as u8, amount: utxo.amount, }).collect(), fee: p.fee, } } } fn utxos() -> Vec { let mut utxos = vec![]; for i in 0..30 { if i < 10 { utxos.push(utxo!(i, 25)); } else if i < 20 { utxos.push(sapling!(i, 25)); } else { utxos.push(orchard!(i, 25)); } } utxos }