use super::*;use near_sdk::test_utils::{get_logs, VMContextBuilder};use near_sdk::{testing_env, PromiseResult, RuntimeFeesConfig};const DOC: &str = "aa00000000000000000000000000000000000000000000000000000000000001";const DAY: u64 = 86_400_000;const T0: u64 = 1_800_000_000_000; // msfn acc(s: &str) -> AccountId { s.parse().unwrap()}fn usdc() -> AccountId { acc("usdc.near")}fn intents() -> AccountId { acc("intents.near")}fn merchant() -> AccountId { acc("studio.near")}fn tax() -> AccountId { acc("tax.near")}fn designer() -> AccountId { acc("designer.near")}fn ctx(who: AccountId, deposit: NearToken, now_ms: u64) -> VMContextBuilder { let mut b = VMContextBuilder::new(); b.current_account_id(acc("dulypay.near")) .signer_account_id(acc("relayer.near")) .predecessor_account_id(who) .attached_deposit(deposit) .prepaid_gas(Gas::from_tgas(300)) .block_timestamp(now_ms * 1_000_000) .block_height(1000); b}fn call_as(who: AccountId, deposit: NearToken, now_ms: u64) { testing_env!(ctx(who, deposit, now_ms).build());}fn yocto1() -> NearToken { NearToken::from_yoctonear(1)}fn zero() -> NearToken { NearToken::from_yoctonear(0)}fn setup() -> Duly { call_as(merchant(), REGISTER_DEPOSIT, T0); let mut c = Duly::new(usdc(), intents()); assert_eq!(c.register(), 1); c}fn invoice(c: &mut Duly, amount: u128) -> String { call_as(merchant(), INVOICE_DEPOSIT, T0); c.create_invoice(U128(amount), U64(T0 + 30 * DAY), DOC.into(), None, None).id}fn pay(c: &mut Duly, msg: &str, amount: u128) -> U128 { pay_at(c, msg, amount, T0 + DAY)}fn pay_at(c: &mut Duly, msg: &str, amount: u128, now: u64) -> U128 { call_as(usdc(), zero(), now); c.ft_on_transfer(intents(), U128(amount), msg.to_string())}fn with_tax_and_designer(c: &mut Duly) { call_as(merchant(), PAYEE_ROW_DEPOSIT.saturating_mul(2), T0); c.set_shares(vec![ Share { to: tax(), bps: 2000, label: "Tax".into() }, Share { to: designer(), bps: 1000, label: "Designer".into() }, ]);}fn status(c: &Duly, id: &str) -> String { c.get_invoice(id.into()).unwrap().status}fn bal(c: &Duly, a: AccountId) -> u128 { c.balance_of(a).0}// ---------------------------------------------------------------- id handling#[test]fn ids_round_trip_and_reject_garbage() { let k = invoice_key(7, 42); assert_eq!(format_id(k), "7-42"); assert_eq!(parse_id("7-42"), Some(k)); for bad in ["", "7", "-", "7-", "-42", "a-b", "7-42-1", "99999999999-1", "1-99999999999"] { assert_eq!(parse_id(bad), None, "{bad}"); }}#[test]fn hex_is_strict() { assert!(hex_32(DOC).is_some()); assert!(hex_32(&DOC[1..]).is_none()); assert!(hex_32(&DOC.to_uppercase()).is_none()); assert_eq!(to_hex(&hex_32(DOC).unwrap()), DOC);}// --------------------------------------------------------------- registration#[test]#[should_panic(expected = "Attach the register deposit")]fn register_needs_deposit() { call_as(merchant(), zero(), T0); Duly::new(usdc(), intents()).register();}#[test]#[should_panic(expected = "Already registered")]fn register_twice_fails() { let mut c = setup(); call_as(merchant(), REGISTER_DEPOSIT, T0); c.register();}#[test]fn register_deposit_covers_full_merchant_record() { let mut c = setup(); let before = env::storage_usage(); // worst case: longest account ids, four shares, longest labels call_as(acc(&"a".repeat(64)), REGISTER_DEPOSIT, T0); c.register(); let long = |i: u8| -> AccountId { acc(&format!("{}", (b'b' + i) as char).repeat(64)) }; let shares: Vec<Share> = (0..4) .map(|i| Share { to: long(i), bps: 100, label: "x".repeat(MAX_LABEL_BYTES) }) .collect(); call_as(acc(&"a".repeat(64)), PAYEE_ROW_DEPOSIT.saturating_mul(4), T0); c.set_shares(shares); call_as(acc(&"a".repeat(64)), yocto1(), T0); c.set_agent(Some(long(4)), 100, Some(long(5)), U128(1)); c.merchants.flush(); c.mids.flush(); c.balances.flush(); let used = env::storage_usage() - before; let paid = (REGISTER_DEPOSIT.as_yoctonear() + 4 * PAYEE_ROW_DEPOSIT.as_yoctonear()) / 10u128.pow(19); assert!((used as u128) <= paid, "used {used} bytes, paid for {paid}");}#[test]fn invoice_deposit_covers_invoice_with_all_payment_records() { let mut c = setup(); let id = invoice(&mut c, 3_000_000); c.invoices.flush(); let before = env::storage_usage(); let longest = "x".repeat(MAX_REFUND_TO_BYTES); for _ in 0..MAX_PAYMENTS { pay(&mut c, &format!("{id}|{longest}"), 1_000_000); } c.invoices.flush(); let grew = env::storage_usage() - before; // 40 bytes of trie overhead per key plus the key and the empty row let row = 40 + 64 + 8 + 200; let paid = INVOICE_DEPOSIT.as_yoctonear() / 10u128.pow(19); assert!(row + grew as u128 <= paid, "row {row} + grew {grew} > paid {paid}");}// ------------------------------------------------------------------- invoices#[test]fn numbers_are_sequential_per_merchant() { let mut c = setup(); call_as(acc("other.near"), REGISTER_DEPOSIT, T0); c.register(); assert_eq!(invoice(&mut c, 1_000_000), "1-1"); assert_eq!(invoice(&mut c, 1_000_000), "1-2"); call_as(acc("other.near"), INVOICE_DEPOSIT, T0); let v = c.create_invoice(U128(MIN_INVOICE), U64(T0 + 1), DOC.into(), None, None); assert_eq!((v.id.as_str(), v.number), ("2-1", 1)); assert_eq!(invoice(&mut c, 1_000_000), "1-3");}#[test]#[should_panic(expected = "Not a merchant")]fn invoice_needs_merchant() { let mut c = setup(); call_as(acc("stranger.near"), INVOICE_DEPOSIT, T0); c.create_invoice(U128(MIN_INVOICE), U64(T0 + 1), DOC.into(), None, None);}#[test]#[should_panic(expected = "Attach the invoice deposit")]fn invoice_needs_deposit() { let mut c = setup(); call_as(merchant(), zero(), T0); c.create_invoice(U128(MIN_INVOICE), U64(T0 + 1), DOC.into(), None, None);}#[test]#[should_panic(expected = "below the minimum")]fn invoice_minimum() { let mut c = setup(); call_as(merchant(), INVOICE_DEPOSIT, T0); c.create_invoice(U128(MIN_INVOICE - 1), U64(T0 + 1), DOC.into(), None, None);}#[test]#[should_panic(expected = "above the maximum")]fn invoice_maximum() { let mut c = setup(); call_as(merchant(), INVOICE_DEPOSIT, T0); c.create_invoice(U128(MAX_INVOICE + 1), U64(T0 + 1), DOC.into(), None, None);}#[test]#[should_panic(expected = "Due date is in the past")]fn invoice_due_date_in_past() { let mut c = setup(); call_as(merchant(), INVOICE_DEPOSIT, T0); c.create_invoice(U128(MIN_INVOICE), U64(T0 - 1), DOC.into(), None, None);}#[test]#[should_panic(expected = "Bad document hash")]fn invoice_bad_hash() { let mut c = setup(); call_as(merchant(), INVOICE_DEPOSIT, T0); c.create_invoice(U128(MIN_INVOICE), U64(T0 + 1), "abc".into(), None, None);}#[test]fn document_fingerprint_is_fixed_when_the_invoice_is_issued() { // The fingerprint (SHA-256 of the encrypted line items) is written once by // create_invoice. Paying, refunding and discounting never touch it, and no method // takes a new one, so the pay page can always compare the document against it. let mut c = setup(); let id = invoice(&mut c, 100_000_000); assert_eq!(c.get_invoice(id.clone()).unwrap().doc_hash, DOC); call_as(merchant(), zero(), T0); c.offer_discount(id.clone(), 500, U64(T0 + DAY)); pay(&mut c, &id, 40_000_000); pay(&mut c, &id, 60_000_000); call_as(merchant(), yocto1(), T0 + 2 * DAY); let _ = c.refund(id.clone(), U128(1_000_000), Dest::Near { account: acc("client.near") }); assert_eq!(c.get_invoice(id).unwrap().doc_hash, DOC);}#[test]#[should_panic(expected = "Bad document hash")]fn document_fingerprint_must_be_64_lowercase_hex() { let mut c = setup(); call_as(merchant(), INVOICE_DEPOSIT, T0); c.create_invoice(U128(MIN_INVOICE), U64(T0 + 1), DOC.to_uppercase(), None, None);}#[test]fn list_pages_through_invoices() { let mut c = setup(); for _ in 0..5 { invoice(&mut c, 1_000_000); } let page = c.list_invoices(merchant(), 2, 2); assert_eq!(page.iter().map(|v| v.number).collect::<Vec<_>>(), vec![2, 3]); assert_eq!(c.list_invoices(merchant(), 0, 100).len(), 5); assert!(c.list_invoices(acc("nobody.near"), 1, 10).is_empty());}// ----------------------------------------------------------------- settlement#[test]fn exact_payment_closes_invoice_in_the_same_call() { let mut c = setup(); let id = invoice(&mut c, 100_000_000); assert_eq!(pay(&mut c, &id, 100_000_000).0, 0); let v = c.get_invoice(id.clone()).unwrap(); assert_eq!(v.status, "paid"); assert_eq!(v.due_now.0, 0); assert_eq!(v.closed_at_ms.0, T0 + DAY); assert_eq!(bal(&c, merchant()), 100_000_000); assert_eq!(c.get_totals().liabilities.0, 100_000_000); let log = get_logs().join(""); assert!(log.contains("EVENT_JSON")); assert!(log.contains("\"closed\":true"));}#[test]fn set_asides_happen_in_the_settlement_step() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 100_000_000); assert_eq!(bal(&c, tax()), 20_000_000); assert_eq!(bal(&c, designer()), 10_000_000); assert_eq!(bal(&c, merchant()), 70_000_000); assert_eq!(c.get_totals().liabilities.0, 100_000_000); let log = get_logs().join(""); assert!(log.contains("\"label\":\"Tax\"")); assert!(log.contains("\"net\":\"70000000\""));}#[test]fn rounding_dust_stays_with_merchant_and_nothing_is_lost() { let mut c = setup(); call_as(merchant(), PAYEE_ROW_DEPOSIT.saturating_mul(2), T0); c.set_shares(vec![ Share { to: tax(), bps: 3333, label: "Tax".into() }, Share { to: designer(), bps: 3333, label: "D".into() }, ]); let id = invoice(&mut c, 10_001); pay(&mut c, &id, 10_001); assert_eq!(bal(&c, tax()), 3_333); assert_eq!(bal(&c, designer()), 3_333); assert_eq!(bal(&c, merchant()), 3_335);}#[test]fn partial_payments_add_up_then_close() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 40_000_000); let v = c.get_invoice(id.clone()).unwrap(); assert_eq!((v.status.as_str(), v.due_now.0), ("partial", 60_000_000)); pay(&mut c, &id, 60_000_000); let v = c.get_invoice(id.clone()).unwrap(); assert_eq!((v.status.as_str(), v.due_now.0), ("paid", 0)); assert_eq!(bal(&c, tax()), 20_000_000); assert_eq!(bal(&c, merchant()), 70_000_000); assert_eq!(v.payments.len(), 2);}#[test]fn overpayment_is_kept_apart_from_set_asides() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 130_000_000); let v = c.get_invoice(id).unwrap(); assert_eq!((v.status.as_str(), v.excess.0), ("paid", 30_000_000)); assert_eq!(bal(&c, tax()), 20_000_000); assert_eq!(bal(&c, designer()), 10_000_000); assert_eq!(bal(&c, merchant()), 100_000_000);}#[test]fn double_payment_is_held_as_excess_not_split_twice() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 100_000_000); pay(&mut c, &id, 100_000_000); let v = c.get_invoice(id).unwrap(); assert_eq!((v.status.as_str(), v.excess.0), ("paid", 100_000_000)); assert_eq!(bal(&c, tax()), 20_000_000); assert_eq!(bal(&c, merchant()), 170_000_000);}#[test]fn unknown_invoice_goes_straight_back() { let mut c = setup(); for msg in ["", "garbage", "9-9", "1-1", "1-1|x", "{\"a\":1}", "-", "99999999999-1"] { assert_eq!(pay(&mut c, msg, 5_000_000).0, 5_000_000, "{msg}"); } assert_eq!(c.get_totals().liabilities.0, 0);}#[test]#[should_panic(expected = "Only USDC is accepted")]fn other_tokens_are_refused() { let mut c = setup(); let id = invoice(&mut c, 1_000_000); call_as(acc("fake-usdc.near"), zero(), T0); c.ft_on_transfer(intents(), U128(1_000_000), id);}#[test]#[should_panic(expected = "Only USDC is accepted")]fn a_direct_call_cannot_fake_a_payment() { let mut c = setup(); let id = invoice(&mut c, 1_000_000); call_as(merchant(), zero(), T0); c.ft_on_transfer(merchant(), U128(1_000_000), id);}#[test]fn payment_to_cancelled_invoice_is_kept_for_refund() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 1_000_000); call_as(merchant(), zero(), T0); c.cancel_invoice(id.clone()); assert_eq!(pay(&mut c, &id, 1_000_000).0, 0); let v = c.get_invoice(id).unwrap(); assert_eq!((v.status.as_str(), v.excess.0), ("cancelled", 1_000_000)); assert_eq!(bal(&c, tax()), 0); assert_eq!(bal(&c, merchant()), 1_000_000);}#[test]fn refund_destination_is_recorded_and_sanitised() { let mut c = setup(); let id = invoice(&mut c, 3_000_000); pay(&mut c, &format!("{id}|base:USDC:0xAbC123"), 1_000_000); pay(&mut c, &format!("{id}|x\"}},{{\"evil\":\"<script>"), 1_000_000); pay(&mut c, &format!("{id}|{}", "y".repeat(MAX_REFUND_TO_BYTES + 1)), 1_000_000); let p = c.get_invoice(id).unwrap().payments; assert_eq!(p[0].refund_to, "base:USDC:0xAbC123"); assert_eq!(p[1].refund_to, ""); assert_eq!(p[2].refund_to, "");}#[test]fn direct_near_payer_becomes_its_own_refund_destination() { let mut c = setup(); let id = invoice(&mut c, 1_000_000); call_as(usdc(), zero(), T0); c.ft_on_transfer(acc("client.near"), U128(1_000_000), id.clone()); assert_eq!(c.get_invoice(id).unwrap().payments[0].refund_to, "near:USDC:client.near");}#[test]fn dust_cannot_fill_the_payment_records() { let mut c = setup(); let id = invoice(&mut c, 100_000_000); for _ in 0..5 { pay(&mut c, &id, 1); } assert_eq!(c.get_invoice(id.clone()).unwrap().payments.len(), 0); pay(&mut c, &format!("{id}|base:USDC:0xabc"), 100_000_000); assert_eq!(c.get_invoice(id).unwrap().payments.len(), 1);}#[test]fn overdue_status_flips_with_time() { let mut c = setup(); let id = invoice(&mut c, 1_000_000); call_as(acc("reader.near"), zero(), T0 + 29 * DAY); assert_eq!(status(&c, &id), "open"); call_as(acc("reader.near"), zero(), T0 + 31 * DAY); assert_eq!(status(&c, &id), "overdue");}// ------------------------------------------------------------------ discounts#[test]fn early_payment_discount_closes_at_the_lower_total() { let mut c = setup(); with_tax_and_designer(&mut c); call_as(merchant(), INVOICE_DEPOSIT, T0); let id = c .create_invoice( U128(100_000_000), U64(T0 + 30 * DAY), DOC.into(), Some(200), Some(U64(T0 + 10 * DAY)), ) .id; call_as(acc("reader.near"), zero(), T0 + 9 * DAY); assert_eq!(c.get_invoice(id.clone()).unwrap().due_now.0, 98_000_000); pay_at(&mut c, &id, 98_000_000, T0 + 9 * DAY); assert_eq!(status(&c, &id), "paid"); assert_eq!(bal(&c, tax()), 19_600_000);}#[test]fn discount_lapses_after_its_window() { let mut c = setup(); call_as(merchant(), INVOICE_DEPOSIT, T0); let id = c .create_invoice( U128(100_000_000), U64(T0 + 30 * DAY), DOC.into(), Some(200), Some(U64(T0 + 10 * DAY)), ) .id; pay_at(&mut c, &id, 98_000_000, T0 + 11 * DAY); let v = c.get_invoice(id).unwrap(); assert_eq!((v.status.as_str(), v.due_now.0), ("partial", 2_000_000));}#[test]fn agent_offer_works_inside_limits() { let mut c = setup(); let agent = acc("agent.near"); call_as(merchant(), yocto1(), T0); c.set_agent(Some(agent.clone()), 300, None, U128(0)); let id = invoice(&mut c, 100_000_000); let late = T0 + 31 * DAY; call_as(agent, zero(), late); c.offer_discount(id.clone(), 300, U64(late + 2 * DAY)); assert_eq!(c.get_invoice(id).unwrap().due_now.0, 97_000_000);}#[test]#[should_panic(expected = "Above the limit you were given")]fn agent_cannot_exceed_the_cap() { let mut c = setup(); let agent = acc("agent.near"); call_as(merchant(), yocto1(), T0); c.set_agent(Some(agent.clone()), 300, None, U128(0)); let id = invoice(&mut c, 100_000_000); let late = T0 + 31 * DAY; call_as(agent, zero(), late); c.offer_discount(id, 301, U64(late + 2 * DAY));}#[test]#[should_panic(expected = "Agent can only act on overdue invoices")]fn agent_cannot_discount_a_current_invoice() { let mut c = setup(); let agent = acc("agent.near"); call_as(merchant(), yocto1(), T0); c.set_agent(Some(agent.clone()), 300, None, U128(0)); let id = invoice(&mut c, 100_000_000); call_as(agent, zero(), T0 + DAY); c.offer_discount(id, 100, U64(T0 + 2 * DAY));}#[test]#[should_panic(expected = "Not your invoice")]fn strangers_cannot_discount() { let mut c = setup(); let id = invoice(&mut c, 100_000_000); call_as(acc("stranger.near"), zero(), T0 + 31 * DAY); c.offer_discount(id, 100, U64(T0 + 32 * DAY));}#[test]#[should_panic(expected = "Offer window too long")]fn offer_window_is_bounded() { let mut c = setup(); let id = invoice(&mut c, 100_000_000); call_as(merchant(), zero(), T0); c.offer_discount(id, 100, U64(T0 + MAX_OFFER_MS + 1));}#[test]#[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")]fn agent_limits_cannot_be_changed_by_a_function_call_key() { let mut c = setup(); call_as(merchant(), zero(), T0); c.set_agent(Some(acc("agent.near")), 5000, None, U128(0));}// -------------------------------------------------------------------- cancel#[test]#[should_panic(expected = "Invoice is already paid")]fn cannot_cancel_a_paid_invoice() { let mut c = setup(); let id = invoice(&mut c, 2_000_000); pay(&mut c, &id, 2_000_000); call_as(merchant(), zero(), T0); c.cancel_invoice(id);}#[test]fn a_dust_payment_cannot_block_cancelling() { let mut c = setup(); let id = invoice(&mut c, 2_000_000); pay(&mut c, &id, 1); call_as(merchant(), zero(), T0); c.cancel_invoice(id.clone()); let v = c.get_invoice(id).unwrap(); assert_eq!((v.status.as_str(), v.due_now.0), ("cancelled", 0));}#[test]fn a_discount_can_complete_a_part_paid_invoice() { let mut c = setup(); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 90_000_000); call_as(merchant(), zero(), T0 + DAY); c.offer_discount(id.clone(), 1000, U64(T0 + 2 * DAY)); let v = c.get_invoice(id.clone()).unwrap(); assert_eq!((v.status.as_str(), v.due_now.0), ("paid", 0)); call_as(acc("reader.near"), zero(), T0 + 3 * DAY); assert_eq!(status(&c, &id), "paid");}#[test]fn a_failed_refund_of_excess_leaves_the_numbers_right() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); call_as(merchant(), zero(), T0); c.cancel_invoice(id.clone()); pay(&mut c, &id, 5_000_000); let key = parse_id(&id).unwrap(); call_as(merchant(), yocto1(), T0); let _ = c.refund(id.clone(), U128(5_000_000), Dest::Near { account: acc("client.near") }); callback(vec![PromiseResult::Failed]); c.on_transfer(merchant(), U128(5_000_000), Some(U64(key)), U128(0), Err(PromiseError::Failed)); let v = c.get_invoice(id).unwrap(); assert_eq!((v.applied.0, v.excess.0, v.refunded.0), (0, 5_000_000, 0)); assert_eq!(bal(&c, merchant()), 5_000_000);}#[test]fn a_failed_refund_of_a_part_payment_restores_what_was_applied() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 2_000_000); let key = parse_id(&id).unwrap(); call_as(merchant(), yocto1(), T0); let _ = c.refund(id.clone(), U128(2_000_000), Dest::Near { account: acc("client.near") }); assert_eq!(c.get_invoice(id.clone()).unwrap().due_now.0, 5_000_000); callback(vec![PromiseResult::Failed]); c.on_transfer(merchant(), U128(2_000_000), Some(U64(key)), U128(2_000_000), Err(PromiseError::Failed)); let v = c.get_invoice(id).unwrap(); assert_eq!((v.applied.0, v.due_now.0, v.status.as_str()), (2_000_000, 3_000_000, "partial"));}#[test]#[should_panic(expected = "Not your invoice")]fn cannot_cancel_someone_elses_invoice() { let mut c = setup(); let id = invoice(&mut c, 2_000_000); call_as(acc("stranger.near"), zero(), T0); c.cancel_invoice(id);}// ------------------------------------------------------------------ money out#[test]fn withdraw_debits_before_the_transfer() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.withdraw(U128(2_000_000), Dest::Near { account: acc("bank.near") }); assert_eq!(bal(&c, merchant()), 3_000_000); assert_eq!(c.get_totals().liabilities.0, 3_000_000);}#[test]#[should_panic(expected = "Not enough balance")]fn cannot_withdraw_more_than_balance() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.withdraw(U128(5_000_001), Dest::Near { account: acc("bank.near") });}#[test]#[should_panic(expected = "No balance")]fn strangers_have_nothing_to_withdraw() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(acc("thief.near"), yocto1(), T0); let _ = c.withdraw(U128(1), Dest::Near { account: acc("thief.near") });}#[test]#[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")]fn withdraw_requires_a_full_access_signature() { let mut c = setup(); call_as(merchant(), zero(), T0); let _ = c.withdraw(U128(1), Dest::Near { account: acc("bank.near") });}#[test]#[should_panic(expected = "Attach more gas")]fn withdraw_refuses_too_little_gas() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); let mut b = ctx(merchant(), yocto1(), T0); b.prepaid_gas(Gas::from_tgas(30)); testing_env!(b.build()); let _ = c.withdraw(U128(1), Dest::Intents { deposit_address: acc("abcd.near") });}#[test]fn set_aside_holder_can_withdraw_its_share() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 100_000_000); call_as(designer(), yocto1(), T0); let _ = c.withdraw(U128(10_000_000), Dest::Near { account: designer() }); assert_eq!(bal(&c, designer()), 0);}fn callback(results: Vec<PromiseResult>) { testing_env!( ctx(acc("dulypay.near"), zero(), T0).build(), near_sdk::test_vm_config(), RuntimeFeesConfig::test(), Default::default(), results );}#[test]fn failed_transfer_restores_the_balance() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.withdraw(U128(5_000_000), Dest::Near { account: acc("unregistered.near") }); callback(vec![PromiseResult::Failed]); c.on_transfer(merchant(), U128(5_000_000), None, U128(0), Err(PromiseError::Failed)); assert_eq!(bal(&c, merchant()), 5_000_000); assert_eq!(c.get_totals().liabilities.0, 5_000_000); assert!(get_logs().join("").contains("payout_failed"));}#[test]fn successful_transfer_keeps_the_debit() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.withdraw(U128(5_000_000), Dest::Near { account: acc("bank.near") }); callback(vec![PromiseResult::Successful(vec![])]); c.on_transfer(merchant(), U128(5_000_000), None, U128(0), Ok(())); assert_eq!(bal(&c, merchant()), 0);}#[test]fn intents_payout_restores_only_the_unused_part() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.withdraw(U128(5_000_000), Dest::Intents { deposit_address: acc("abcd.near") }); callback(vec![PromiseResult::Successful(b"\"3000000\"".to_vec())]); c.on_transfer_call(merchant(), U128(5_000_000), None, U128(0), Ok(U128(3_000_000))); assert_eq!(bal(&c, merchant()), 2_000_000);}#[test]fn intents_payout_failure_restores_everything() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.withdraw(U128(5_000_000), Dest::Intents { deposit_address: acc("abcd.near") }); callback(vec![PromiseResult::Failed]); c.on_transfer_call(merchant(), U128(5_000_000), None, U128(0), Err(PromiseError::Failed)); assert_eq!(bal(&c, merchant()), 5_000_000);}#[test]fn jar_can_release_to_the_merchant_without_any_token_call() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 100_000_000); call_as(tax(), yocto1(), T0); c.send(merchant(), U128(5_000_000)); assert_eq!(bal(&c, tax()), 15_000_000); assert_eq!(bal(&c, merchant()), 75_000_000); assert_eq!(c.get_totals().liabilities.0, 100_000_000);}#[test]#[should_panic(expected = "That account has no balance here yet")]fn send_cannot_grow_storage_for_free() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); c.send(acc("new.near"), U128(1));}#[test]#[should_panic(expected = "Not enough balance")]fn send_is_limited_by_balance() { let mut c = setup(); with_tax_and_designer(&mut c); call_as(tax(), yocto1(), T0); c.send(merchant(), U128(1));}#[test]fn per_invoice_set_aside_total_is_recorded() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 40_000_000); pay(&mut c, &id, 90_000_000); let v = c.get_invoice(id).unwrap(); assert_eq!(v.set_aside.0, 30_000_000); assert_eq!(v.excess.0, 30_000_000);}// -------------------------------------------------------------------- refunds#[test]fn full_refund_flips_status_and_failure_undoes_it() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let key = parse_id(&id).unwrap(); let _ = c.refund(id.clone(), U128(5_000_000), Dest::Intents { deposit_address: acc("ab.near") }); assert_eq!(status(&c, &id), "refunded"); assert_eq!(bal(&c, merchant()), 0); callback(vec![PromiseResult::Failed]); c.on_transfer_call(merchant(), U128(5_000_000), Some(U64(key)), U128(0), Err(PromiseError::Failed)); assert_eq!(status(&c, &id), "paid"); assert_eq!(bal(&c, merchant()), 5_000_000);}#[test]fn partial_refund_of_a_closed_invoice() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.refund(id.clone(), U128(1_000_000), Dest::Near { account: acc("client.near") }); let v = c.get_invoice(id).unwrap(); assert_eq!((v.status.as_str(), v.refunded.0), ("partially_refunded", 1_000_000));}#[test]fn refund_of_a_part_payment_reopens_what_is_owed() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 2_000_000); call_as(merchant(), yocto1(), T0); let _ = c.refund(id.clone(), U128(2_000_000), Dest::Near { account: acc("client.near") }); let v = c.get_invoice(id).unwrap(); assert_eq!(v.due_now.0, 5_000_000); assert_eq!(v.status, "refunded");}#[test]fn refunding_an_overpayment_clears_the_excess() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 7_000_000); assert_eq!(c.get_invoice(id.clone()).unwrap().excess.0, 2_000_000); call_as(merchant(), yocto1(), T0); let _ = c.refund(id.clone(), U128(2_000_000), Dest::Near { account: acc("client.near") }); let v = c.get_invoice(id).unwrap(); assert_eq!(v.excess.0, 0); assert_eq!(v.status, "partially_refunded");}#[test]#[should_panic(expected = "Refund above what was paid")]fn cannot_refund_more_than_paid() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.refund(id, U128(5_000_001), Dest::Near { account: acc("client.near") });}#[test]#[should_panic(expected = "Refund above what was paid")]fn cannot_refund_twice() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(merchant(), yocto1(), T0); let _ = c.refund(id.clone(), U128(5_000_000), Dest::Near { account: acc("client.near") }); call_as(merchant(), yocto1(), T0); let _ = c.refund(id, U128(1), Dest::Near { account: acc("client.near") });}#[test]#[should_panic(expected = "Not your invoice")]fn strangers_cannot_refund() { let mut c = setup(); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(acc("thief.near"), yocto1(), T0); let _ = c.refund(id, U128(1), Dest::Near { account: acc("thief.near") });}#[test]#[should_panic(expected = "Not enough balance")]fn refund_needs_the_money_in_the_merchant_balance() { let mut c = setup(); with_tax_and_designer(&mut c); let id = invoice(&mut c, 100_000_000); pay(&mut c, &id, 100_000_000); call_as(merchant(), yocto1(), T0); let _ = c.refund(id, U128(100_000_000), Dest::Near { account: acc("client.near") });}// ---------------------------------------------------------------------- sweep#[test]fn agent_sweep_goes_only_to_the_named_account_above_the_floor() { let mut c = setup(); let agent = acc("agent.near"); call_as(merchant(), yocto1(), T0); c.set_agent(Some(agent.clone()), 0, Some(acc("vault.near")), U128(1_000_000)); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(agent, zero(), T0); let _ = c.agent_sweep(merchant()); assert_eq!(bal(&c, merchant()), 1_000_000); assert!(get_logs().join("").contains("vault.near"));}#[test]#[should_panic(expected = "Not the appointed agent")]fn only_the_agent_can_sweep() { let mut c = setup(); call_as(merchant(), yocto1(), T0); c.set_agent(Some(acc("agent.near")), 0, Some(acc("vault.near")), U128(0)); call_as(acc("thief.near"), zero(), T0); let _ = c.agent_sweep(merchant());}#[test]#[should_panic(expected = "Nothing above the floor")]fn sweep_respects_the_floor() { let mut c = setup(); let agent = acc("agent.near"); call_as(merchant(), yocto1(), T0); c.set_agent(Some(agent.clone()), 0, Some(acc("vault.near")), U128(5_000_000)); let id = invoice(&mut c, 5_000_000); pay(&mut c, &id, 5_000_000); call_as(agent, zero(), T0); let _ = c.agent_sweep(merchant());}// -------------------------------------------------------------------- set_shares#[test]#[should_panic(expected = "Set-asides add up to more than 100%")]fn shares_cannot_exceed_everything() { let mut c = setup(); call_as(merchant(), PAYEE_ROW_DEPOSIT.saturating_mul(2), T0); c.set_shares(vec![ Share { to: tax(), bps: 6000, label: "Tax".into() }, Share { to: designer(), bps: 4001, label: "D".into() }, ]);}#[test]#[should_panic(expected = "Duplicate set-aside")]fn shares_cannot_repeat_a_payee() { let mut c = setup(); call_as(merchant(), PAYEE_ROW_DEPOSIT.saturating_mul(2), T0); c.set_shares(vec![ Share { to: tax(), bps: 100, label: "A".into() }, Share { to: tax(), bps: 100, label: "B".into() }, ]);}#[test]#[should_panic(expected = "Attach the deposit for new payees")]fn new_payee_rows_are_paid_for() { let mut c = setup(); call_as(merchant(), zero(), T0); c.set_shares(vec![Share { to: tax(), bps: 100, label: "A".into() }]);}#[test]#[should_panic(expected = "Label may use letters")]fn labels_are_plain_text() { let mut c = setup(); call_as(merchant(), PAYEE_ROW_DEPOSIT, T0); c.set_shares(vec![Share { to: tax(), bps: 100, label: "<b>".into() }]);}// ------------------------------------------------------------------ invariant/// Deterministic pseudo-random walk over every money path. After every step the/// sum of balances must equal what came in minus what left.#[test]fn balances_always_add_up() { let mut c = setup(); with_tax_and_designer(&mut c); let holders = [merchant(), tax(), designer()]; let mut x: u64 = 0x9e3779b97f4a7c15; let mut rnd = |m: u64| { x ^= x << 13; x ^= x >> 7; x ^= x << 17; x % m }; let mut ids: Vec<String> = vec![]; let mut inflow: u128 = 0; let mut outflow: u128 = 0; for _ in 0..1500 { match rnd(5) { 0 => ids.push(invoice(&mut c, 10_000 + rnd(50_000_000) as u128)), 1 | 2 if !ids.is_empty() => { let id = ids[rnd(ids.len() as u64) as usize].clone(); let amount = 1 + rnd(30_000_000) as u128; pay(&mut c, &id, amount); inflow += amount; } 3 => { let who = holders[rnd(3) as usize].clone(); let have = bal(&c, who.clone()); if have > 0 { let amount = 1 + rnd(have as u64) as u128; call_as(who.clone(), yocto1(), T0); let _ = c.withdraw(U128(amount), Dest::Near { account: acc("bank.near") }); if rnd(4) == 0 { callback(vec![PromiseResult::Failed]); c.on_transfer(who, U128(amount), None, U128(0), Err(PromiseError::Failed)); } else { outflow += amount; } } } _ if !ids.is_empty() => { let id = ids[rnd(ids.len() as u64) as usize].clone(); let v = c.get_invoice(id.clone()).unwrap(); let room = v.paid.0 - v.refunded.0; let have = bal(&c, merchant()); let max = room.min(have); if max > 0 { let amount = 1 + rnd(max as u64) as u128; call_as(merchant(), yocto1(), T0); let _ = c.refund(id, U128(amount), Dest::Near { account: acc("client.near") }); outflow += amount; } } _ => {} } let sum: u128 = holders.iter().map(|h| bal(&c, h.clone())).sum(); assert_eq!(sum, inflow - outflow); assert_eq!(c.get_totals().liabilities.0, sum); } assert!(inflow > 0 && outflow > 0);}