duly / contract/src/tests.rs · raw
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);}