duly / contract/src/lib.rs · raw
//! Duly settlement contract.//!//! A client pays from any chain through NEAR Intents. The USDC arrives here by//! `ft_transfer_call` with the invoice id in `msg` (1Click `customRecipientMsg`),//! so the payment itself closes the invoice. In that same step the merchant's//! set-asides (tax, subcontractors) move into separate balances.//!//! Design rules://! - The code has no owner role, pause or upgrade method: through this code nobody//!   can move a balance except the account that holds it (and, within limits that//!   account set, its appointed agent, which can only sweep to the account the//!   holder named). An upgrade key is kept on the contract account during the//!   hackathon and could replace this code, so "no admin" is not claimed.//! - Balances are internal. `ft_on_transfer` never makes an external call, so a//!   payment cannot fail because some recipient is unregistered.//! - Every state-growing method is paid for by an attached deposit, so strangers//!   cannot drain the contract's storage balance.//! - Funds move out only after the balance is debited; a failed transfer restores it.use near_sdk::json_types::{U128, U64};use near_sdk::store::LookupMap;use near_sdk::{    env, near, require, AccountId, BorshStorageKey, Gas, NearToken, PanicOnDefault, Promise,    PromiseError,};/// Basis points in 100%.const BPS: u128 = 10_000;/// Set-aside rules per merchant.const MAX_SHARES: usize = 4;/// Payments recorded per invoice with their refund destination. Later ones still/// count towards the invoice and always appear in the event log.const MAX_PAYMENTS: usize = 3;const MAX_LABEL_BYTES: usize = 24;const MAX_REFUND_TO_BYTES: usize = 80;/// Smallest invoice: 0.01 USDC.const MIN_INVOICE: u128 = 10_000;/// Largest invoice: 10,000 USDC. The contract is unaudited; this is a deliberate ceiling.const MAX_INVOICE: u128 = 10_000_000_000;/// Longest window a discount offer can stay open: 30 days.const MAX_OFFER_MS: u64 = 30 * 24 * 60 * 60 * 1000;const REGISTER_DEPOSIT: NearToken = NearToken::from_millinear(10);const INVOICE_DEPOSIT: NearToken = NearToken::from_millinear(6);const PAYEE_ROW_DEPOSIT: NearToken = NearToken::from_millinear(2);const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1);const GAS_FT_TRANSFER: Gas = Gas::from_tgas(15);const GAS_FT_TRANSFER_CALL: Gas = Gas::from_tgas(100);const GAS_CALLBACK: Gas = Gas::from_tgas(15);const GAS_OVERHEAD: Gas = Gas::from_tgas(10);#[derive(BorshStorageKey)]#[near]enum StorageKey {    Merchants,    Mids,    Balances,    Invoices,}/// One set-aside rule: `bps` of every settled amount goes to `to`.#[near(serializers = [borsh, json])]#[derive(Clone, Debug, PartialEq)]pub struct Share {    pub to: AccountId,    pub bps: u16,    pub label: String,}/// Where money leaves the contract to.#[near(serializers = [borsh, json])]#[derive(Clone, Debug, PartialEq)]pub enum Dest {    /// USDC to a NEAR account (it must be registered on the USDC contract).    Near { account: AccountId },    /// USDC into a NEAR Intents deposit account from a 1Click quote    /// (`depositType: INTENTS`), which then pays out on any chain.    Intents { deposit_address: AccountId },}#[near(serializers = [borsh])]pub struct Merchant {    mid: u32,    /// Last invoice number issued. Numbers run 1, 2, 3 with no gaps.    seq: u32,    shares: Vec<Share>,    agent: Option<AccountId>,    /// Largest discount the agent may offer on an overdue invoice.    agent_max_discount_bps: u16,    /// The only place an agent sweep can send money.    sweep_to: Option<AccountId>,    /// An agent sweep leaves at least this much in the balance.    sweep_keep: u128,}#[near(serializers = [borsh])]pub struct Payment {    amount: u128,    at_ms: u64,    block: u64,    /// "<chain>:<symbol>:<address>" from the payer, used for refunds.    refund_to: String,}#[near(serializers = [borsh])]pub struct Invoice {    amount: u128,    /// Everything received, including any excess.    paid: u128,    /// The part of `paid` applied to the invoice and split by set-asides.    applied: u128,    /// How much of `applied` went to set-asides.    set_aside: u128,    refunded: u128,    issued_at_ms: u64,    due_at_ms: u64,    closed_at_ms: u64,    doc_hash: [u8; 32],    discount_bps: u16,    discount_until_ms: u64,    cancelled: bool,    payments: Vec<Payment>,}#[near(serializers = [json])]pub struct PaymentView {    pub amount: U128,    pub at_ms: U64,    pub block: U64,    pub refund_to: String,}#[near(serializers = [json])]pub struct InvoiceView {    pub id: String,    pub number: u32,    pub merchant: AccountId,    pub amount: U128,    /// What the client owes right now, after any active discount.    pub due_now: U128,    pub paid: U128,    pub applied: U128,    pub excess: U128,    pub set_aside: U128,    pub refunded: U128,    pub status: String,    pub issued_at_ms: U64,    pub due_at_ms: U64,    pub closed_at_ms: U64,    pub doc_hash: String,    pub discount_bps: u16,    pub discount_until_ms: U64,    pub payments: Vec<PaymentView>,}#[near(serializers = [json])]pub struct MerchantView {    pub mid: u32,    pub invoices: u32,    pub balance: U128,    pub shares: Vec<Share>,    pub agent: Option<AccountId>,    pub agent_max_discount_bps: u16,    pub sweep_to: Option<AccountId>,    pub sweep_keep: U128,}#[near(serializers = [json])]pub struct Totals {    /// USDC this contract owes its balance holders. Compare with `ft_balance_of`.    pub liabilities: U128,    pub received: U128,    pub merchants: u32,    pub invoices: U64,}#[near(serializers = [json])]pub struct Config {    pub usdc: AccountId,    pub intents: AccountId,    pub register_deposit: U128,    pub invoice_deposit: U128,    pub payee_row_deposit: U128,    pub min_invoice: U128,    pub max_invoice: U128,}#[near(contract_state)]#[derive(PanicOnDefault)]pub struct Duly {    usdc: AccountId,    intents: AccountId,    merchants: LookupMap<AccountId, Merchant>,    mids: LookupMap<u32, AccountId>,    balances: LookupMap<AccountId, u128>,    invoices: LookupMap<u64, Invoice>,    last_mid: u32,    invoice_count: u64,    liabilities: u128,    received: u128,}fn invoice_key(mid: u32, seq: u32) -> u64 {    ((mid as u64) << 32) | seq as u64}fn parse_id(id: &str) -> Option<u64> {    let (mid, seq) = id.split_once('-')?;    if mid.len() > 10 || seq.len() > 10 {        return None;    }    Some(invoice_key(mid.parse().ok()?, seq.parse().ok()?))}fn format_id(key: u64) -> String {    let mut s = (key >> 32).to_string();    s.push('-');    s.push_str(&(key as u32).to_string());    s}fn hex_32(s: &str) -> Option<[u8; 32]> {    let b = s.as_bytes();    if b.len() != 64 {        return None;    }    let digit = |c: u8| match c {        b'0'..=b'9' => Some(c - b'0'),        b'a'..=b'f' => Some(c - b'a' + 10),        _ => None,    };    let mut out = [0u8; 32];    for i in 0..32 {        out[i] = digit(b[2 * i])? << 4 | digit(b[2 * i + 1])?;    }    Some(out)}fn to_hex(b: &[u8]) -> String {    const H: &[u8; 16] = b"0123456789abcdef";    let mut s = String::with_capacity(b.len() * 2);    for x in b {        s.push(H[(x >> 4) as usize] as char);        s.push(H[(x & 15) as usize] as char);    }    s}/// Refund destinations come from the payer, so they are untrusted. Only a plain/// charset is stored; anything else is dropped rather than failing the payment.fn clean_refund_to(s: &str) -> String {    let ok = s.len() <= MAX_REFUND_TO_BYTES        && s.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b':' | b'.' | b'_' | b'-'));    if ok {        s.to_string()    } else {        String::new()    }}/// NEP-297 event. `data` is a JSON object built by the callers below from account/// ids, numbers and validated labels only, so nothing in it needs escaping.fn emit(event: &str, data: String) {    env::log_str(&format!(        r#"EVENT_JSON:{{"standard":"duly","version":"1.0.0","event":"{event}","data":[{data}]}}"#    ));}fn opt_json(a: &Option<AccountId>) -> String {    a.as_ref().map_or("null".to_string(), |a| format!(r#""{a}""#))}fn dest_json(d: &Dest) -> String {    match d {        Dest::Near { account } => format!(r#"{{"near":"{account}"}}"#),        Dest::Intents { deposit_address } => format!(r#"{{"intents":"{deposit_address}"}}"#),    }}impl Invoice {    /// Total the client must pay at `now`, after an active discount.    fn payable_total(&self, now: u64) -> u128 {        if self.discount_bps > 0 && now <= self.discount_until_ms {            self.amount - self.amount * self.discount_bps as u128 / BPS        } else {            self.amount        }    }    fn due_now(&self, now: u64) -> u128 {        if self.cancelled || self.closed_at_ms > 0 {            0        } else {            self.payable_total(now).saturating_sub(self.applied)        }    }    fn status(&self, now: u64) -> &'static str {        if self.paid > 0 && self.refunded >= self.paid {            "refunded"        } else if self.cancelled {            "cancelled"        } else if self.closed_at_ms > 0 {            if self.refunded > 0 {                "partially_refunded"            } else {                "paid"            }        } else if self.paid > 0 {            "partial"        } else if now > self.due_at_ms {            "overdue"        } else {            "open"        }    }}#[near]impl Duly {    #[init]    pub fn new(usdc: AccountId, intents: AccountId) -> Self {        Self {            usdc,            intents,            merchants: LookupMap::new(StorageKey::Merchants),            mids: LookupMap::new(StorageKey::Mids),            balances: LookupMap::new(StorageKey::Balances),            invoices: LookupMap::new(StorageKey::Invoices),            last_mid: 0,            invoice_count: 0,            liabilities: 0,            received: 0,        }    }    // ---------------------------------------------------------------- merchant    /// Open a merchant record. The deposit pays for its storage.    #[payable]    pub fn register(&mut self) -> u32 {        require!(env::attached_deposit() >= REGISTER_DEPOSIT, "Attach the register deposit");        let who = env::predecessor_account_id();        require!(!self.merchants.contains_key(&who), "Already registered");        self.last_mid += 1;        let mid = self.last_mid;        self.mids.insert(mid, who.clone());        self.balances.entry(who.clone()).or_insert(0);        self.merchants.insert(            who.clone(),            Merchant {                mid,                seq: 0,                shares: vec![],                agent: None,                agent_max_discount_bps: 0,                sweep_to: None,                sweep_keep: 0,            },        );        emit("merchant_registered", format!(r#"{{"merchant":"{who}","mid":{mid}}}"#));        mid    }    /// Replace the set-aside rules. Applies to every payment from now on.    /// Each payee that has no balance row yet costs one PAYEE_ROW_DEPOSIT.    #[payable]    pub fn set_shares(&mut self, shares: Vec<Share>) {        let who = env::predecessor_account_id();        require!(self.merchants.contains_key(&who), "Not a merchant");        require!(shares.len() <= MAX_SHARES, "Too many set-asides");        let mut total: u32 = 0;        for (i, s) in shares.iter().enumerate() {            require!(s.bps > 0, "A set-aside cannot be zero");            require!(s.label.len() <= MAX_LABEL_BYTES, "Label too long");            require!(                s.label.bytes().all(|c| c.is_ascii_alphanumeric() || c == b' ' || c == b'-'),                "Label may use letters, numbers, spaces and hyphens"            );            require!(s.to != who, "A set-aside cannot point at yourself");            require!(shares[..i].iter().all(|o| o.to != s.to), "Duplicate set-aside");            total += s.bps as u32;        }        require!(total as u128 <= BPS, "Set-asides add up to more than 100%");        let new_rows = shares.iter().filter(|s| !self.balances.contains_key(&s.to)).count() as u128;        require!(            env::attached_deposit().as_yoctonear()                >= new_rows * PAYEE_ROW_DEPOSIT.as_yoctonear(),            "Attach the deposit for new payees"        );        for s in &shares {            self.balances.entry(s.to.clone()).or_insert(0);        }        let lines: Vec<String> = shares            .iter()            .map(|s| format!(r#"{{"to":"{}","bps":{},"label":"{}"}}"#, s.to, s.bps, s.label))            .collect();        emit("shares_set", format!(r#"{{"merchant":"{who}","shares":[{}]}}"#, lines.join(",")));        self.merchants.get_mut(&who).unwrap().shares = shares;    }    /// Issue an invoice. Only the amount, dates and a hash of the encrypted    /// document go on-chain: client names and line items stay private.    #[payable]    pub fn create_invoice(        &mut self,        amount: U128,        due_at_ms: U64,        doc_hash: String,        discount_bps: Option<u16>,        discount_until_ms: Option<U64>,    ) -> InvoiceView {        require!(env::attached_deposit() >= INVOICE_DEPOSIT, "Attach the invoice deposit");        let who = env::predecessor_account_id();        let now = env::block_timestamp_ms();        require!(amount.0 >= MIN_INVOICE, "Invoice is below the minimum");        require!(amount.0 <= MAX_INVOICE, "Invoice is above the maximum");        require!(due_at_ms.0 >= now, "Due date is in the past");        let doc_hash = hex_32(&doc_hash).unwrap_or_else(|| env::panic_str("Bad document hash"));        let discount_bps = discount_bps.unwrap_or(0);        let discount_until_ms = discount_until_ms.map_or(0, |v| v.0);        require!(discount_bps as u128 <= BPS / 2, "Discount above 50%");        require!(            discount_bps == 0 || discount_until_ms <= now.saturating_add(MAX_OFFER_MS),            "Discount window too long"        );        let merchant =            self.merchants.get_mut(&who).unwrap_or_else(|| env::panic_str("Not a merchant"));        merchant.seq += 1;        let key = invoice_key(merchant.mid, merchant.seq);        let number = merchant.seq;        self.invoice_count += 1;        self.invoices.insert(            key,            Invoice {                amount: amount.0,                paid: 0,                applied: 0,                set_aside: 0,                refunded: 0,                issued_at_ms: now,                due_at_ms: due_at_ms.0,                closed_at_ms: 0,                doc_hash,                discount_bps,                discount_until_ms,                cancelled: false,                payments: vec![],            },        );        emit(            "invoice_created",            format!(                r#"{{"id":"{}","number":{number},"merchant":"{who}","amount":"{}"}}"#,                format_id(key),                amount.0            ),        );        self.view_of(key).unwrap()    }    /// Stop an invoice collecting money. Anyone can pay a little into any invoice,    /// so only a fully paid invoice blocks this. Anything already received stays    /// refundable, and later payments are held as excess for refund.    pub fn cancel_invoice(&mut self, id: String) {        let key = self.owned_key(&id);        let inv = self.invoices.get_mut(&key).unwrap();        require!(inv.closed_at_ms == 0, "Invoice is already paid: refund it instead");        require!(!inv.cancelled, "Invoice already cancelled");        inv.cancelled = true;        emit("invoice_cancelled", format!(r#"{{"id":"{}"}}"#, format_id(key)));    }    /// Offer a discount if the client pays by `until_ms`.    /// The merchant may offer up to 50%. The appointed agent may only touch    /// overdue invoices and only up to the cap the merchant set.    pub fn offer_discount(&mut self, id: String, bps: u16, until_ms: U64) {        let key = parse_id(&id).unwrap_or_else(|| env::panic_str("Bad invoice id"));        let owner = self.owner_of(key);        let merchant = self.merchants.get(&owner).unwrap();        let caller = env::predecessor_account_id();        let now = env::block_timestamp_ms();        let inv = self.invoices.get(&key).unwrap_or_else(|| env::panic_str("No such invoice"));        require!(!inv.cancelled && inv.closed_at_ms == 0, "Invoice is closed");        require!(until_ms.0 > now, "Offer already expired");        require!(until_ms.0 <= now.saturating_add(MAX_OFFER_MS), "Offer window too long");        if caller == owner {            require!(bps as u128 <= BPS / 2, "Discount above 50%");        } else {            require!(merchant.agent.as_ref() == Some(&caller), "Not your invoice");            require!(now > inv.due_at_ms, "Agent can only act on overdue invoices");            require!(bps <= merchant.agent_max_discount_bps, "Above the limit you were given");        }        let inv = self.invoices.get_mut(&key).unwrap();        inv.discount_bps = bps;        inv.discount_until_ms = until_ms.0;        // What has been paid may already cover the discounted total.        if inv.applied > 0 && inv.applied >= inv.payable_total(now) {            inv.closed_at_ms = now;        }        emit(            "discount_offered",            format!(                r#"{{"id":"{}","bps":{bps},"until_ms":"{}","by":"{caller}"}}"#,                format_id(key),                until_ms.0            ),        );    }    /// Appoint or remove the agent and set its limits. One yocto is required so    /// a function-call key, which cannot attach deposits, can never widen them.    #[payable]    pub fn set_agent(        &mut self,        agent: Option<AccountId>,        max_discount_bps: u16,        sweep_to: Option<AccountId>,        sweep_keep: U128,    ) {        near_sdk::assert_one_yocto();        require!(max_discount_bps as u128 <= BPS / 2, "Discount above 50%");        let who = env::predecessor_account_id();        let m = self.merchants.get_mut(&who).unwrap_or_else(|| env::panic_str("Not a merchant"));        m.agent = agent.clone();        m.agent_max_discount_bps = max_discount_bps;        m.sweep_to = sweep_to.clone();        m.sweep_keep = sweep_keep.0;        emit(            "agent_set",            format!(                r#"{{"merchant":"{who}","agent":{},"max_discount_bps":{max_discount_bps},"sweep_to":{},"sweep_keep":"{}"}}"#,                opt_json(&agent),                opt_json(&sweep_to),                sweep_keep.0            ),        );    }    // ------------------------------------------------------------- settlement    /// NEP-141 receiver. `msg` is "<invoice id>" or "<invoice id>|<refund_to>".    /// Returns the amount to send back: everything for an unknown invoice, nothing    /// otherwise. A known invoice always keeps the money, even if it is cancelled    /// or already paid, so a late or double payment can be refunded to its payer    /// instead of bouncing through a bridge. No external calls happen here.    pub fn ft_on_transfer(&mut self, sender_id: AccountId, amount: U128, msg: String) -> U128 {        require!(env::predecessor_account_id() == self.usdc, "Only USDC is accepted");        let (id, refund_to) = msg.split_once('|').unwrap_or((msg.as_str(), ""));        let Some(key) = parse_id(id).filter(|k| self.invoices.contains_key(k)) else {            env::log_str("Unknown invoice: sending the money back");            return amount;        };        let owner = self.owner_of(key);        let now = env::block_timestamp_ms();        let block = env::block_height();        let mut refund_to = clean_refund_to(refund_to);        if refund_to.is_empty() && sender_id != self.intents {            refund_to = format!("near:USDC:{sender_id}");        }        let inv = self.invoices.get_mut(&key).unwrap();        let due = inv.due_now(now);        let applied = amount.0.min(due);        let excess = amount.0 - applied;        inv.paid += amount.0;        inv.applied += applied;        if inv.payments.len() < MAX_PAYMENTS && amount.0.saturating_mul(100) >= inv.amount {            inv.payments.push(Payment { amount: amount.0, at_ms: now, block, refund_to });        }        let closed_now = !inv.cancelled && inv.closed_at_ms == 0 && applied == due && due > 0;        if closed_now {            inv.closed_at_ms = now;        }        let status = inv.status(now);        // Set-asides apply to the invoiced amount only, never to an excess that        // may have to go back to the payer. Rounding remainders stay with the merchant.        let shares = self.merchants.get(&owner).unwrap().shares.clone();        let mut net = applied;        let mut lines = Vec::with_capacity(shares.len());        for s in &shares {            let part = applied * s.bps as u128 / BPS;            if part > 0 {                net -= part;                self.credit(&s.to, part);                lines.push(format!(r#"{{"to":"{}","label":"{}","amount":"{part}"}}"#, s.to, s.label));            }        }        self.credit(&owner, net + excess);        self.received += amount.0;        self.invoices.get_mut(&key).unwrap().set_aside += applied - net;        emit(            "invoice_payment",            format!(                r#"{{"id":"{}","merchant":"{owner}","amount":"{}","applied":"{applied}","excess":"{excess}","net":"{net}","set_asides":[{}],"status":"{status}","closed":{closed_now},"block":{block}}}"#,                format_id(key),                amount.0,                lines.join(",")            ),        );        U128(0)    }    // -------------------------------------------------------------- money out    /// Move your balance out, to a NEAR account or through Intents to any chain.    #[payable]    pub fn withdraw(&mut self, amount: U128, to: Dest) -> Promise {        near_sdk::assert_one_yocto();        let who = env::predecessor_account_id();        require!(amount.0 > 0, "Nothing to withdraw");        self.debit(&who, amount.0);        emit("withdrawal", format!(r#"{{"from":"{who}","amount":"{}","to":{}}}"#, amount.0, dest_json(&to)));        self.pay_out(who, amount.0, to, None, 0)    }    /// Move your balance to another balance in this contract. A tax jar uses this    /// to release money back to the merchant. The target must already have a row.    #[payable]    pub fn send(&mut self, to: AccountId, amount: U128) {        near_sdk::assert_one_yocto();        let who = env::predecessor_account_id();        require!(amount.0 > 0, "Nothing to send");        require!(self.balances.contains_key(&to), "That account has no balance here yet");        self.debit(&who, amount.0);        self.credit(&to, amount.0);        emit("internal_transfer", format!(r#"{{"from":"{who}","to":"{to}","amount":"{}"}}"#, amount.0));    }    /// Refund a client out of the merchant's balance.    #[payable]    pub fn refund(&mut self, id: String, amount: U128, to: Dest) -> Promise {        near_sdk::assert_one_yocto();        let key = self.owned_key(&id);        let who = env::predecessor_account_id();        let inv = self.invoices.get_mut(&key).unwrap();        require!(amount.0 > 0, "Nothing to refund");        require!(amount.0 <= inv.paid - inv.refunded, "Refund above what was paid");        inv.refunded += amount.0;        // Refunding a part payment reopens what is owed. Excess was never applied.        let reopened = if inv.closed_at_ms == 0 { amount.0.min(inv.applied) } else { 0 };        inv.applied -= reopened;        self.debit(&who, amount.0);        emit(            "invoice_refund",            format!(r#"{{"id":"{}","amount":"{}","to":{}}}"#, format_id(key), amount.0, dest_json(&to)),        );        self.pay_out(who, amount.0, to, Some(key), reopened)    }    /// Agent-only: move everything above the merchant's floor to the one account    /// the merchant named. The agent cannot choose the destination or the floor.    pub fn agent_sweep(&mut self, merchant: AccountId) -> Promise {        let m = self.merchants.get(&merchant).unwrap_or_else(|| env::panic_str("Not a merchant"));        require!(            m.agent.as_ref() == Some(&env::predecessor_account_id()),            "Not the appointed agent"        );        let to = m.sweep_to.clone().unwrap_or_else(|| env::panic_str("No sweep account set"));        let keep = m.sweep_keep;        let amount = self.balance_of(merchant.clone()).0.saturating_sub(keep);        require!(amount > 0, "Nothing above the floor");        self.debit(&merchant, amount);        emit("sweep", format!(r#"{{"merchant":"{merchant}","amount":"{amount}","to":"{to}"}}"#));        self.pay_out(merchant, amount, Dest::Near { account: to }, None, 0)    }    #[private]    pub fn on_transfer(        &mut self,        from: AccountId,        amount: U128,        key: Option<U64>,        reopened: U128,        #[callback_result] result: Result<(), PromiseError>,    ) {        if result.is_err() {            self.restore(&from, amount.0, key.map(|k| k.0), reopened.0);        }    }    #[private]    pub fn on_transfer_call(        &mut self,        from: AccountId,        amount: U128,        key: Option<U64>,        reopened: U128,        #[callback_result] used: Result<U128, PromiseError>,    ) {        let used = used.map_or(0, |u| u.0).min(amount.0);        if used < amount.0 {            self.restore(&from, amount.0 - used, key.map(|k| k.0), reopened.0);        }    }    // ------------------------------------------------------------------ views    pub fn get_invoice(&self, id: String) -> Option<InvoiceView> {        self.view_of(parse_id(&id)?)    }    /// Invoices of one merchant by number, oldest first from `from_number`.    pub fn list_invoices(        &self,        merchant: AccountId,        from_number: u32,        limit: u32,    ) -> Vec<InvoiceView> {        let Some(m) = self.merchants.get(&merchant) else { return vec![] };        let start = from_number.max(1);        (start..=m.seq)            .take(limit.min(50) as usize)            .filter_map(|n| self.view_of(invoice_key(m.mid, n)))            .collect()    }    pub fn get_merchant(&self, account: AccountId) -> Option<MerchantView> {        let m = self.merchants.get(&account)?;        Some(MerchantView {            mid: m.mid,            invoices: m.seq,            balance: self.balance_of(account.clone()),            shares: m.shares.clone(),            agent: m.agent.clone(),            agent_max_discount_bps: m.agent_max_discount_bps,            sweep_to: m.sweep_to.clone(),            sweep_keep: U128(m.sweep_keep),        })    }    pub fn balance_of(&self, account: AccountId) -> U128 {        U128(self.balances.get(&account).copied().unwrap_or(0))    }    pub fn get_totals(&self) -> Totals {        Totals {            liabilities: U128(self.liabilities),            received: U128(self.received),            merchants: self.last_mid,            invoices: U64(self.invoice_count),        }    }    pub fn get_config(&self) -> Config {        Config {            usdc: self.usdc.clone(),            intents: self.intents.clone(),            register_deposit: U128(REGISTER_DEPOSIT.as_yoctonear()),            invoice_deposit: U128(INVOICE_DEPOSIT.as_yoctonear()),            payee_row_deposit: U128(PAYEE_ROW_DEPOSIT.as_yoctonear()),            min_invoice: U128(MIN_INVOICE),            max_invoice: U128(MAX_INVOICE),        }    }}impl Duly {    fn owner_of(&self, key: u64) -> AccountId {        self.mids            .get(&((key >> 32) as u32))            .cloned()            .unwrap_or_else(|| env::panic_str("No such invoice"))    }    /// Parse an id and require the caller to own that invoice.    fn owned_key(&self, id: &str) -> u64 {        let key = parse_id(id).unwrap_or_else(|| env::panic_str("Bad invoice id"));        require!(self.invoices.contains_key(&key), "No such invoice");        require!(self.owner_of(key) == env::predecessor_account_id(), "Not your invoice");        key    }    fn view_of(&self, key: u64) -> Option<InvoiceView> {        let inv = self.invoices.get(&key)?;        let now = env::block_timestamp_ms();        Some(InvoiceView {            id: format_id(key),            number: key as u32,            merchant: self.owner_of(key),            amount: U128(inv.amount),            due_now: U128(inv.due_now(now)),            paid: U128(inv.paid),            applied: U128(inv.applied),            excess: U128((inv.paid - inv.refunded).saturating_sub(inv.applied)),            set_aside: U128(inv.set_aside),            refunded: U128(inv.refunded),            status: inv.status(now).to_string(),            issued_at_ms: U64(inv.issued_at_ms),            due_at_ms: U64(inv.due_at_ms),            closed_at_ms: U64(inv.closed_at_ms),            doc_hash: to_hex(&inv.doc_hash),            discount_bps: inv.discount_bps,            discount_until_ms: U64(inv.discount_until_ms),            payments: inv                .payments                .iter()                .map(|p| PaymentView {                    amount: U128(p.amount),                    at_ms: U64(p.at_ms),                    block: U64(p.block),                    refund_to: p.refund_to.clone(),                })                .collect(),        })    }    /// Only called for accounts that already have a row, so a payment can never    /// grow storage beyond what deposits paid for.    fn credit(&mut self, who: &AccountId, amount: u128) {        let row = self.balances.get_mut(who).unwrap_or_else(|| env::panic_str("No balance row"));        *row += amount;        self.liabilities += amount;    }    fn debit(&mut self, who: &AccountId, amount: u128) {        let row = self.balances.get_mut(who).unwrap_or_else(|| env::panic_str("No balance"));        require!(*row >= amount, "Not enough balance");        *row -= amount;        self.liabilities -= amount;    }    fn restore(&mut self, who: &AccountId, amount: u128, key: Option<u64>, reopened: u128) {        self.credit(who, amount);        if let Some(inv) = key.and_then(|k| self.invoices.get_mut(&k)) {            inv.refunded -= amount.min(inv.refunded);            if inv.closed_at_ms == 0 {                inv.applied += reopened.min(amount);                let now = env::block_timestamp_ms();                if !inv.cancelled && inv.applied > 0 && inv.applied >= inv.payable_total(now) {                    inv.closed_at_ms = now;                }            }        }        emit("payout_failed", format!(r#"{{"from":"{who}","amount":"{amount}"}}"#));    }    fn pay_out(        &self,        from: AccountId,        amount: u128,        to: Dest,        key: Option<u64>,        reopened: u128,    ) -> Promise {        let key_arg = key.map(U64);        let (args, gas, method) = match &to {            Dest::Near { account } => (                format!(r#"{{"receiver_id":"{account}","amount":"{amount}"}}"#),                GAS_FT_TRANSFER,                "ft_transfer",            ),            Dest::Intents { deposit_address } => (                format!(                    r#"{{"receiver_id":"{}","amount":"{amount}","msg":"{deposit_address}"}}"#,                    self.intents                ),                GAS_FT_TRANSFER_CALL,                "ft_transfer_call",            ),        };        let needed = gas.as_gas() + GAS_CALLBACK.as_gas() + GAS_OVERHEAD.as_gas();        require!(env::prepaid_gas().as_gas() >= needed, "Attach more gas");        let call = Promise::new(self.usdc.clone()).function_call(            method.to_string(),            args.into_bytes(),            ONE_YOCTO,            gas,        );        let this = Self::ext(env::current_account_id()).with_static_gas(GAS_CALLBACK);        match to {            Dest::Near { .. } => {                call.then(this.on_transfer(from, U128(amount), key_arg, U128(reopened)))            }            Dest::Intents { .. } => {                call.then(this.on_transfer_call(from, U128(amount), key_arg, U128(reopened)))            }        }    }}#[cfg(test)]mod tests;