Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

invoices.gno

36.00 Kb · 919 lines
  1// Package invoices is on-chain accounts receivable: an issuer records an
  2// obligation, and the named payer discharges it later, in parts.
  3//
  4// # The one idea
  5//
  6// Every payment realm in this portfolio, and every escrow and vesting
  7// package the discovery gate found on either live chain, is PREFUNDED.
  8// service_market escrows the exact price at purchase. subscriptions
  9// prepays a whole period. payroll, vesting, fee_split and revshare push
 10// money that already arrived. In all of them the coins exist before the
 11// claim on them does.
 12//
 13// An invoice inverts that. It is UNFUNDED AT CREATION: the obligation is
 14// recorded first, and the money may never arrive at all.
 15//
 16// That one inversion is the whole realm, and it is visible in the
 17// arithmetic rather than only in the prose. payroll's invariant carries a
 18// pool term because an employer funds first:
 19//
 20//	payroll:  Held() == TotalPooled() + Liabilities() + Surplus()
 21//	invoices: Held() ==                 Liabilities() + Surplus()
 22//
 23// There is no pool here, and no reserve, because at the moment an invoice
 24// is issued this realm holds nothing on its behalf and promises nothing.
 25// Issuing costs the issuer a storage deposit and nothing else.
 26//
 27// # What being unfunded forces
 28//
 29// Two features that would be decoration on a prefunded rail are load-
 30// bearing here, and they are the reason this is a realm rather than a
 31// field on an existing one:
 32//
 33//   - PARTIAL SETTLEMENT. When the money is already in escrow, "pay" is a
 34//     release of a known amount and partiality is meaningless. When it is
 35//     not, a payer who can send 60% today is the ordinary case. Pay
 36//     accepts any amount up to the outstanding balance, repeatedly.
 37//   - AN OVERDUE STATE. A prefunded claim cannot be late. An unfunded one
 38//     can, so lateness is a state other realms can read and act on. That
 39//     readable state is the integration surface this realm exists to
 40//     provide, in the same way subscriptions exists to provide Entitled.
 41//
 42// # No escrow, no acceptance, no arbitration
 43//
 44// An invoice is billed for work already done, so there is nothing for this
 45// realm to hold back. Money paid is credited to the issuer's ledger
 46// balance in the same transaction. There is no acceptance window and no
 47// release step — those belong to service_market, whose model is the
 48// opposite one.
 49//
 50// Consequently this realm DOES NOT arbitrate. A payer who disputes a bill
 51// simply does not pay it, and the invoice goes overdue. That is the
 52// mechanism off chain too, and inventing a dispute path here would make
 53// this a worse copy of service_market rather than a complement to it.
 54//
 55// # Payment is pull, in the direction that cannot fail
 56//
 57// Pay credits the issuer's ledger balance; the issuer later calls Claim.
 58// The push alternative — paying the issuer inside the payer's own
 59// transaction — would let the issuer's address decide whether the PAYER's
 60// settlement succeeds. An issuer that is a realm which panics on receipt,
 61// or is simply unable to accept, would make its own invoices unpayable and
 62// the failure would land on the payer. Pull puts every failure on the
 63// party who owns it.
 64//
 65// # Anyone may issue an invoice naming anyone. Read this before trusting a total.
 66//
 67// There is no permission to bill someone, because requiring one would mean
 68// a registry of who may invoice whom, which is a different application.
 69// The honest consequence is stated here rather than discovered later:
 70//
 71//	AN UNPAID INVOICE IS AN UNVERIFIED CLAIM BY ITS ISSUER.
 72//
 73// So this realm deliberately exposes NO global "what does this address
 74// owe" aggregate. Such a number would be the sum of every stranger's
 75// self-reported assertion, and reading it as a credit signal is the error
 76// the getter's existence would invite. What it exposes instead is
 77// ISSUER-SCOPED: OutstandingFrom and OverdueFrom answer "what does this
 78// payer owe THIS issuer", which is the question a consumer realm can
 79// actually act on, because a consumer gating its own service on its own
 80// unpaid bills is trusting only itself.
 81//
 82// Two bounds back that up mechanically. Issuing costs a storage deposit
 83// paid by the ISSUER, so volume is not free to the party who benefits from
 84// it; and MaxRecordsPerIssuer caps one issuer's STORED records, which is an
 85// availability bound on that issuer's own namespace and no one else's.
 86// Neither bound makes an unpaid claim true, and they are not presented as
 87// doing so.
 88//
 89// Both bounds are PER ISSUER, and the global total is not bounded. Sybil
 90// issuers multiply it: N funded addresses yield N*MaxRecordsPerIssuer
 91// records, and the per-record deposit paid by each is the only thing making
 92// that expensive. Two consequences follow, stated here rather than
 93// discovered:
 94//
 95//   - A payer named on an invoice CANNOT remove it. Void is issuer-only,
 96//     because forgiving a debt is the issuer's to do, and Release refuses an
 97//     open record; so a stranger's open invoice naming you persists until
 98//     that stranger voids it. What absorbs this is the design above rather
 99//     than a bound: no global aggregate reads such a record, and the
100//     issuer-scoped getters a consumer realm actually calls stay gas-bounded
101//     at MaxRecordsPerIssuer, so a victim's reads are never charged for an
102//     attacker's volume. The residual harm is an off-chain reader's, and the
103//     unverified-claim warning above is what it is for.
104//   - An issuer record is never reclaimed. Release decrements its count but
105//     does not delete the issuers entry, because `issued` and `invoiced` are
106//     LIFETIME figures that IssuedOf reports, and deleting the entry would
107//     reset them to zero and make that getter lie. Global state therefore
108//     grows monotonically. Keeping an honest counter is worth more than
109//     reclaiming one AVL node.
110//
111// The cap counts STORED records rather than OPEN ones, and the difference
112// is load-bearing rather than a detail. OutstandingFrom, OverdueFrom and
113// HasOverdueFrom — the getters a consumer realm calls, and therefore the
114// ones whose gas cost is someone else's problem — iterate an issuer's whole
115// prefix, settled and voided records included, because a settled record is
116// not removed until Release. A cap on open invoices alone would bound the
117// obligations and leave that iteration unbounded: an issuer could issue and
118// void repeatedly, keeping `open` at zero while their prefix grew without
119// limit, until a consumer's read ran out of gas. Capping what is stored
120// bounds both, since open can never exceed stored.
121//
122// Finding "every invoice naming me" is therefore an off-chain indexer's
123// job: on chain it would be an unbounded iteration over every issuer that
124// has ever billed anyone, and the answer would be untrustworthy anyway.
125//
126// # STORAGE
127//
128// An issuer record is paid for by that issuer's first Issue; an invoice
129// record by the Issue that creates it. Release removes a finished invoice
130// and the chain refunds the freed deposit to the transaction that frees
131// it, which is the standing incentive to tidy up a record that is no
132// longer anyone's business.
133//
134// # What this realm does NOT do
135//
136//   - It does not verify that work happened, value it, or arbitrate.
137//   - It holds ugnot and only ugnot.
138//   - It has no admin. The deploying namespace receives protocol fees and
139//     swept surplus and has no other privilege: no setter, no pause, no
140//     ability to void an invoice or touch a balance.
141package invoices
142
143import (
144	"chain"
145	"chain/runtime"
146	"chain/runtime/unsafe"
147	"strconv"
148	"strings"
149
150	"gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio"
151	"gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger"
152	"gno.land/p/nt/avl/v0"
153	"gno.land/p/nt/markdown/sanitize/v0"
154)
155
156// RealmPath is this realm's own path, used to build Render links.
157const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices"
158
159// Denom is the only asset this realm holds.
160const Denom = "ugnot"
161
162// MaxFeeBps is the hard ceiling on any fee this realm could ever charge,
163// enforced by the feeledger the realm is constructed with — a feeBps above
164// it is refused by the primitive's own validation, not by a check in this
165// file. 500 bps = 5%.
166const MaxFeeBps = int64(500)
167
168// SettlementFeeBps is charged on each payment, on the gross, floor-rounded
169// in the ISSUER's favour: 100 bps = 1%. It matches the most conservative
170// live fee in this portfolio (fee_split, crowdfund, budgets, payroll).
171//
172// The ISSUER bears it, not the payer: a payment of N reduces the
173// outstanding balance by exactly N, and the issuer is credited N minus the
174// fee. A payer's debt must be discharged by what the payer sent, or the
175// arithmetic of "paid in full" would depend on this realm's fee schedule.
176//
177// It is a compile-time constant with no setter, so an invoice's economics
178// cannot be changed after it is issued.
179//
180// The fee is floored PER PAYMENT, so splitting a payment can lower the total
181// fee. Each payment boundary forfeits at most 1 ugnot, and MinPayment bounds
182// how many boundaries an invoice admits. MEASURED on onyx-1, not reasoned:
183// 10,990 paid at once charges 109, while ten payments of 1,099 charge 100.
184// The beneficiary is the ISSUER, who is credited the difference and does not
185// control the payment schedule; the payer gains nothing, because the
186// outstanding balance falls by the gross either way. Gas for one extra
187// payment costs more than the 1 ugnot it saves, so this is priced out rather
188// than prevented, and a colluding issuer/payer pair would spend more to
189// exploit it than they recovered.
190//
191// A final settlement below the fee denominator pays zero fee for that same
192// reason and is NOT a separate case: floor(99/100) is 0 whether or not
193// MinPayment's settle-exactly clause admitted it, so that clause forfeits
194// nothing on its own.
195const SettlementFeeBps = int64(100)
196
197// FeeCollector receives withdrawn fees and swept surplus. Compile-time
198// constant: there is no setter and no transfer path. It is the deploying
199// namespace's address — stated plainly: the operator of this realm.
200//
201// NOTE FOR ANY RE-NAMESPACED PORT: this is the address-as-data hazard that
202// a namespace substitution rewrites silently while every byte-comparison
203// still reports the tree faithful, because the rewrite IS the intended
204// transformation. Re-state it deliberately per chain; never let it be
205// derived.
206const FeeCollector = address("g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3")
207
208// MinInvoiceAmount is the smallest invoice that may be issued. Below the
209// fee denominator a 1% fee floors to zero, so a smaller invoice would be
210// fee-free by accident rather than by design.
211const MinInvoiceAmount = int64(1_000)
212
213// MinPayment keeps dust settlements out. It is NOT a floor on the final
214// payment: a payment is accepted when it is at least MinPayment OR when it
215// settles the invoice exactly. Without that second clause an invoice with
216// 300 ugnot outstanding could never be closed, and "unpayable remainder"
217// is a worse failure than dust.
218const MinPayment = int64(1_000)
219
220// MaxRecordsPerIssuer caps the invoice records one issuer may have STORED
221// at once — open, settled and voided alike. It is what makes the
222// issuer-scoped getters gas-bounded; see the package doc for why the cap
223// counts stored records and not open ones.
224//
225// Issue is permissionless, so this is an AVAILABILITY BOUND and it is
226// documented rather than hidden: an issuer can exhaust their OWN capacity
227// and no one else's, because the bound is per-issuer. The cost of filling
228// it is gas plus a storage deposit on every record.
229//
230// Hitting it is self-service to clear: Release is permissionless and the
231// chain refunds the freed deposit to whoever calls it, so an issuer at the
232// cap can always release their own finished records, and a stranger has a
233// standing incentive to do it for them.
234const MaxRecordsPerIssuer = 64
235
236// MaxMemoLen bounds the free-text memo, in bytes. The memo is rendered,
237// so it is sanitized at the render boundary, never at the write boundary —
238// storing what the issuer actually wrote keeps the record honest.
239const MaxMemoLen = 256
240
241// Invoice statuses.
242const (
243	StatusOpen    = "open"
244	StatusSettled = "settled"
245	StatusVoided  = "voided"
246)
247
248type invoice struct {
249	id     int64
250	issuer address
251	payer  address
252
253	// amount is the gross obligation. It never changes: forgiving part of
254	// a bill is a Void of the whole remainder, not a rewrite of history.
255	amount int64
256
257	// paid is the cumulative gross the payer has sent. It only ever
258	// increases, and amount-paid is the outstanding balance.
259	paid int64
260
261	// dueHeight is the height at or before which the invoice should be
262	// settled. Equal to the issue height means "due on receipt".
263	dueHeight int64
264
265	issuedAt int64
266	status   string
267	memo     string
268}
269
270type issuerRec struct {
271	// records is how many invoice records this issuer currently has
272	// STORED, of any status. It is the figure MaxRecordsPerIssuer caps,
273	// and therefore the one that bounds every issuer-scoped getter: it
274	// rises on Issue and falls only on Release.
275	records int
276
277	// open is how many of those are still open. Informational — a
278	// settled or voided record still occupies a slot until Release, so
279	// this never gates anything.
280	open int
281
282	issued   int64
283	invoiced int64
284}
285
286var (
287	self address
288
289	// invoices holds one record per invoice, keyed issuer|paddedID so an
290	// issuer's book is a bounded prefix iteration. The public handle is
291	// therefore (issuer, id), the same two-part shape payroll uses.
292	invoices = avl.NewTree() // "issuer|%019d" -> *invoice
293
294	// issuers holds one record per address that has ever issued.
295	issuers = avl.NewTree() // address string -> *issuerRec
296
297	// ledger holds every issuer's settled-but-unclaimed balance and the
298	// fee pot. It is global across invoices on purpose: an issuer billing
299	// twenty payers has one balance and one Claim.
300	ledger = feeledger.MustNew(MaxFeeBps)
301
302	// nextID is a monotonic counter. IDs are never reused, so a released
303	// invoice's id can never be confused with a live one.
304	nextID int64 = 1
305)
306
307func init() {
308	self = unsafe.CurrentRealm().Address()
309}
310
311// rejectStraySend aborts when coins are attached to a non-payable call.
312// This realm holds funds, so a stray send would become sweepable surplus,
313// silently converting a user's coins into operator funds.
314func rejectStraySend(cur realm) {
315	if !cur.IsCurrent() {
316		// Every call site forwards a crossing entrypoint's own live cur,
317		// so this cannot fire today; it exists so a future refactor that
318		// hands this guard a stale realm value fails closed, not open
319		// (coinio names IsCurrent as the guard secondary realm parameters
320		// require, and this helper follows that discipline).
321		panic("invoices: realm capability is not current")
322	}
323	if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {
324		panic("this entrypoint does not accept coins")
325	}
326}
327
328// --- issuing ---
329
330// Issue records an obligation of `amount` owed by `payer` to the caller,
331// to be settled by `dueHeight`. It moves no coins and reserves nothing.
332// Returns the invoice id.
333//
334// dueHeight must not be in the past. A backdated due height would create
335// an invoice that is overdue in the transaction that issued it, which is a
336// claim about a deadline that was never offered.
337func Issue(cur realm, payer address, amount, dueHeight int64, memo string) int64 {
338	rejectStraySend(cur)
339
340	// The lowercase clause is load-bearing, and it is carried from budgets'
341	// Grant and payroll's Hire (measured on this toolchain, not argued):
342	// address.IsValid() defers to crypto.AddressFromBech32, whose decoder
343	// lowercases the input BEFORE verifying the checksum, so it returns
344	// true for the all-uppercase spelling of a valid address.
345	//
346	// THE SEVERITY HERE IS THE MILDER OF THE TWO THIS PORTFOLIO HAS
347	// MEASURED, and saying which matters. In payroll an uppercase employee
348	// is a PERMANENT LOSS: Accrue credits a balance nobody can authenticate
349	// as. Here it is a BRICKED RECORD, as in budgets: Pay authenticates the
350	// caller against the stored payer, so a non-canonical payer makes the
351	// invoice unpayable by everyone, including the person it names. No coins
352	// move, nothing is lost but the issuer's own storage deposit and slot.
353	// The guard is still worth its line — a record that can never be
354	// satisfied and can never be distinguished from a real debt is exactly
355	// the untrustworthy total the package doc warns about — but it is not
356	// the fund-loss case and must not be cited as one.
357	//
358	// The check is complete as well as necessary: that decoder rejects
359	// mixed case outright, so a valid address has exactly two spellings and
360	// refusing every non-lower one leaves precisely the canonical one.
361	if !payer.IsValid() || string(payer) != strings.ToLower(string(payer)) {
362		panic("payer is not a valid address in canonical (lowercase) form")
363	}
364	if amount < MinInvoiceAmount {
365		panic("amount is below MinInvoiceAmount")
366	}
367	if len(memo) > MaxMemoLen {
368		panic("memo exceeds MaxMemoLen")
369	}
370	now := runtime.ChainHeight()
371	if dueHeight < now {
372		panic("dueHeight is in the past")
373	}
374
375	issuer := cur.Previous().Address()
376	if issuer == payer {
377		panic("an issuer cannot invoice themselves")
378	}
379	key := issuer.String()
380	r := getIssuer(key)
381	if r == nil {
382		r = &issuerRec{}
383		issuers.Set(key, r)
384	}
385	if r.records >= MaxRecordsPerIssuer {
386		panic("issuer has too many stored invoices")
387	}
388	invoiced, ok := checkedAdd(r.invoiced, amount)
389	if !ok {
390		panic("issuer lifetime total would overflow")
391	}
392
393	id := nextID
394	nextID++
395	invoices.Set(invKey(issuer, id), &invoice{
396		id:        id,
397		issuer:    issuer,
398		payer:     payer,
399		amount:    amount,
400		paid:      0,
401		dueHeight: dueHeight,
402		issuedAt:  now,
403		status:    StatusOpen,
404		memo:      memo,
405	})
406	r.records++
407	r.open++
408	r.issued++
409	r.invoiced = invoiced
410
411	chain.Emit("Issued", "id", itoa(id), "issuer", key,
412		"payer", payer.String(), "amount", itoa(amount),
413		"dueHeight", itoa(dueHeight))
414	return id
415}
416
417// --- settling ---
418
419// Pay settles part or all of an invoice with the attached coins. The named
420// payer only. Direct EOA calls with -send only (the receipt-guaranteed
421// shape); exactly one positive ugnot coin.
422//
423// The attached amount must not exceed the outstanding balance. Accepting
424// an overpayment would turn a payment into a donation the payer did not
425// choose to make and this realm has no path to return, so it is refused
426// and the payer keeps their coins.
427//
428// Returns the outstanding balance remaining after this payment.
429func Pay(cur realm, issuer address, id int64) int64 {
430	// Receive first and once, per coinio's envelope contract.
431	from, amount := coinio.Receive(0, cur, Denom)
432
433	inv := mustInvoice(issuer, id)
434	if inv.status != StatusOpen {
435		panic("invoice is not open")
436	}
437	if from != inv.payer {
438		panic("only the named payer may pay this invoice")
439	}
440	outstanding := inv.amount - inv.paid
441	if amount > outstanding {
442		panic("payment exceeds the outstanding balance")
443	}
444	if amount < MinPayment && amount != outstanding {
445		panic("payment is below MinPayment and does not settle the invoice")
446	}
447
448	inv.paid += amount
449	remaining := inv.amount - inv.paid
450	if remaining == 0 {
451		inv.status = StatusSettled
452		mustDecOpen(issuer)
453	}
454	credited, fee := ledger.MustDeposit(issuer.String(), amount, SettlementFeeBps)
455
456	chain.Emit("Paid", "id", itoa(id), "issuer", issuer.String(),
457		"payer", from.String(), "amount", itoa(amount),
458		"credited", itoa(credited), "fee", itoa(fee),
459		"paid", itoa(inv.paid), "outstanding", itoa(remaining),
460		"status", inv.status)
461	return remaining
462}
463
464// Void cancels the unpaid remainder of an invoice. The issuer only — this
465// is an issuer forgiving a debt, and nobody else may forgive it for them.
466//
467// It does NOT reverse payments already made. Coins the payer sent are
468// already credited to the issuer's ledger balance and may already have
469// been claimed; there is no mechanism on chain to take them back, and
470// pretending otherwise in the status would be worse than saying so. A
471// voided invoice therefore keeps its `paid` figure, which is what makes
472// the record readable afterwards.
473//
474// Voiding does not remove the record. Release does that.
475func Void(cur realm, id int64) {
476	rejectStraySend(cur)
477	issuer := cur.Previous().Address()
478	inv := mustInvoice(issuer, id)
479	if inv.status != StatusOpen {
480		panic("invoice is not open")
481	}
482	inv.status = StatusVoided
483	mustDecOpen(issuer)
484	chain.Emit("Voided", "id", itoa(id), "issuer", issuer.String(),
485		"payer", inv.payer.String(), "paid", itoa(inv.paid),
486		"forgiven", itoa(inv.amount-inv.paid))
487}
488
489// Release removes a finished invoice and frees its storage. Permissionless:
490// the chain refunds the freed storage deposit to the caller, which is the
491// standing incentive to tidy up a record that is not your own.
492//
493// It refuses an open invoice, which is what makes it safe to let a stranger
494// call: a Release can never destroy an outstanding obligation. An issuer
495// who wants a record gone must Void it first, and that leaves an event.
496//
497// Releasing frees the issuer's slot against MaxRecordsPerIssuer. That is
498// the only way a slot is ever freed — settling and voiding do not free
499// one, because the record is still stored and still walked by every
500// issuer-scoped getter.
501func Release(cur realm, issuer address, id int64) {
502	rejectStraySend(cur)
503	inv := mustInvoice(issuer, id)
504	if inv.status == StatusOpen {
505		panic("invoice is still open")
506	}
507	invoices.Remove(invKey(issuer, id))
508	r := getIssuer(issuer.String())
509	if r == nil || r.records == 0 {
510		// Unreachable, and a panic rather than a mask on purpose. See
511		// mustDecOpen for the reasoning; the stakes are higher here
512		// because `records` is what the cap reads, so masking would cost
513		// the issuer a slot permanently and silently.
514		panic("invoices: issuer record count desynchronized from the invoice tree")
515	}
516	r.records--
517	chain.Emit("Released", "id", itoa(id), "issuer", issuer.String(),
518		"status", inv.status, "paid", itoa(inv.paid))
519}
520
521// --- claiming ---
522
523// Claim pays the caller their whole settled balance. The issuer only —
524// there is no "claim on behalf of", because that would let a third party
525// choose the moment someone else's coins move.
526func Claim(cur realm) int64 {
527	rejectStraySend(cur)
528	who := cur.Previous().Address()
529	key := who.String()
530	amount := ledger.BalanceOf(key)
531	if amount == 0 {
532		panic("no balance to claim")
533	}
534	// Debit first, per coinio's ordering contract: a panic inside coinio
535	// aborts the transaction and reverts this debit.
536	ledger.MustWithdraw(key, amount)
537	coinio.Payout(0, cur, who, Denom, amount)
538	chain.Emit("Claimed", "account", key, "amount", itoa(amount))
539	return amount
540}
541
542// WithdrawFees moves the accrued fee pot to FeeCollector. Anyone may call
543// it; the destination is a compile-time constant, so the caller cannot
544// influence where the coins go.
545func WithdrawFees(cur realm) int64 {
546	rejectStraySend(cur)
547	fees := ledger.WithdrawFees()
548	if fees == 0 {
549		panic("no fees accrued")
550	}
551	coinio.Payout(0, cur, FeeCollector, Denom, fees)
552	chain.Emit("FeesWithdrawn", "to", FeeCollector.String(), "amount", itoa(fees))
553	return fees
554}
555
556// SweepSurplus sends coins held beyond every obligation to FeeCollector.
557// The reserve passed to coinio is the realm's full liability, so a sweep
558// can never reach a coin an issuer is owed.
559func SweepSurplus(cur realm) int64 {
560	rejectStraySend(cur)
561	swept := coinio.Sweep(0, cur, FeeCollector, Denom, Liabilities())
562	chain.Emit("Swept", "denom", Denom, "to", FeeCollector.String(),
563		"amount", itoa(swept))
564	return swept
565}
566
567// --- the overdue predicate: this realm's integration surface ---
568
569// isOverdue is a pure function of an invoice and the clock, which is what
570// makes it unit-testable without a chain.
571//
572// Overdue means: still open, still owing, and the due height has PASSED.
573// Strictly passed — at exactly dueHeight the invoice is due but not yet
574// late, which is what lets dueHeight == issue height mean "due on receipt"
575// without being born late.
576func isOverdue(inv *invoice, now int64) bool {
577	return inv.status == StatusOpen && inv.paid < inv.amount && now > inv.dueHeight
578}
579
580// IsOverdue reports whether one invoice is overdue right now.
581func IsOverdue(issuer address, id int64) bool {
582	return isOverdue(mustInvoice(issuer, id), runtime.ChainHeight())
583}
584
585// OutstandingFrom is the total a payer still owes THIS issuer across the
586// issuer's open invoices.
587//
588// The iteration is bounded by MaxRecordsPerIssuer, NOT by the number of
589// open invoices: it walks the issuer's whole stored prefix and skips the
590// records that are not open. That is why the cap counts stored records —
591// see the package doc.
592//
593// This is the issuer-scoped shape the package doc argues for: a consumer
594// realm gating its own service on its own unpaid bills is trusting only
595// itself. There is deliberately no global per-payer total — see the
596// package doc, "Anyone may issue an invoice naming anyone".
597func OutstandingFrom(issuer, payer address) int64 {
598	var sum int64
599	eachOf(issuer, func(inv *invoice) bool {
600		if inv.payer == payer && inv.status == StatusOpen {
601			sum += inv.amount - inv.paid
602		}
603		return false
604	})
605	return sum
606}
607
608// OverdueFrom is OutstandingFrom restricted to invoices whose due height
609// has passed. A consumer realm that wants to withhold service from a late
610// payer reads this.
611func OverdueFrom(issuer, payer address) int64 {
612	now := runtime.ChainHeight()
613	var sum int64
614	eachOf(issuer, func(inv *invoice) bool {
615		if inv.payer == payer && isOverdue(inv, now) {
616			sum += inv.amount - inv.paid
617		}
618		return false
619	})
620	return sum
621}
622
623// HasOverdueFrom is OverdueFrom as a predicate, for a consumer that only
624// needs the boolean. It stops at the first hit rather than summing.
625func HasOverdueFrom(issuer, payer address) bool {
626	now := runtime.ChainHeight()
627	found := false
628	eachOf(issuer, func(inv *invoice) bool {
629		if inv.payer == payer && isOverdue(inv, now) {
630			found = true
631			return true
632		}
633		return false
634	})
635	return found
636}
637
638// --- read-only accessors ---
639
640// InvoiceInfo is the whole record, for a caller that wants one read.
641func InvoiceInfo(issuer address, id int64) (payer address, amount, paid, dueHeight, issuedAt int64, status string) {
642	inv := mustInvoice(issuer, id)
643	return inv.payer, inv.amount, inv.paid, inv.dueHeight, inv.issuedAt, inv.status
644}
645
646// Outstanding is what is still owed on one invoice. Zero for a settled or
647// voided one — a voided invoice owes nothing by definition, which is the
648// point of voiding it.
649func Outstanding(issuer address, id int64) int64 {
650	inv := mustInvoice(issuer, id)
651	if inv.status != StatusOpen {
652		return 0
653	}
654	return inv.amount - inv.paid
655}
656
657// Memo is an invoice's free-text memo, exactly as the issuer wrote it. It
658// is UNSANITIZED and caller-supplied: a consumer that renders it owes it
659// the same treatment Render gives it here.
660func Memo(issuer address, id int64) string { return mustInvoice(issuer, id).memo }
661
662// StatusOf is an invoice's status.
663func StatusOf(issuer address, id int64) string { return mustInvoice(issuer, id).status }
664
665// HasInvoice reports whether a record exists, without panicking on
666// absence — every other invoice accessor panics, which makes them unusable
667// as an existence probe.
668func HasInvoice(issuer address, id int64) bool {
669	return invoices.Get(invKey(issuer, id)) != nil
670}
671
672// OpenOf is the number of open invoices an issuer holds.
673func OpenOf(issuer address) int {
674	if r := getIssuer(issuer.String()); r != nil {
675		return r.open
676	}
677	return 0
678}
679
680// RecordsOf is the number of invoice records an issuer currently has
681// stored, of any status. This — not OpenOf — is the figure capped by
682// MaxRecordsPerIssuer, so it is the one an issuer near the cap needs.
683func RecordsOf(issuer address) int {
684	if r := getIssuer(issuer.String()); r != nil {
685		return r.records
686	}
687	return 0
688}
689
690// IssuedOf is how many invoices an address has ever issued, and their
691// total face value. Note the tense: both are lifetime figures and neither
692// falls when an invoice is settled, voided or released.
693func IssuedOf(issuer address) (count, invoiced int64) {
694	if r := getIssuer(issuer.String()); r != nil {
695		return r.issued, r.invoiced
696	}
697	return 0, 0
698}
699
700// BalanceOf is an issuer's settled, unclaimed balance.
701func BalanceOf(issuer address) int64 { return ledger.BalanceOf(issuer.String()) }
702
703// Records is the number of invoice records currently stored, open or not.
704// It falls on Release, so it is a storage figure rather than a count of
705// invoices that have existed.
706func Records() int { return invoices.Size() }
707
708// Issuers is the number of addresses that have ever issued an invoice.
709func Issuers() int { return issuers.Size() }
710
711// NextID is the id the next Issue will allocate. Exposed so a caller can
712// tell a never-issued id from a released one.
713func NextID() int64 { return nextID }
714
715// SettledTotal is the sum of every issuer's unclaimed balance.
716func SettledTotal() int64 { return ledger.UsersTotal() }
717
718// FeesAccrued is the fee pot awaiting WithdrawFees.
719func FeesAccrued() int64 { return ledger.FeesAccrued() }
720
721// Liabilities is every coin this realm owes to anyone: the ledger's own
722// total, which is issuer balances plus the fee pot.
723//
724// There is no pool term, and its ABSENCE is the unfunded property this
725// realm exists to model — see the package doc. Do not "fix" this to match
726// payroll's shape.
727func Liabilities() int64 { return ledger.Liabilities() }
728
729// Held is the realm's actual ugnot balance, read from the chain.
730func Held() int64 { return coinio.HeldAt(self, Denom) }
731
732// Surplus is coins held beyond every obligation — what SweepSurplus would
733// move. It is zero when every coin arrived through an entrypoint.
734func Surplus() int64 {
735	h, l := Held(), Liabilities()
736	if h <= l {
737		return 0
738	}
739	return h - l
740}
741
742// Address is this realm's own address.
743func Address() address { return self }
744
745// Height is the current chain height, exposed so a caller can compute due
746// heights against the same clock this realm uses.
747func Height() int64 { return runtime.ChainHeight() }
748
749// --- internals ---
750
751// invKey is issuer|paddedID. The id is left-padded to 19 digits — the
752// width of MaxInt64 — so that lexicographic order over an issuer's prefix
753// is numeric order over their invoice ids. An unpadded decimal would sort
754// "10" before "9" and make a prefix scan return them out of order.
755func invKey(issuer address, id int64) string {
756	return issuer.String() + "|" + padID(id)
757}
758
759func padID(id int64) string {
760	s := strconv.FormatInt(id, 10)
761	for len(s) < 19 {
762		s = "0" + s
763	}
764	return s
765}
766
767// eachOf iterates one issuer's invoice records in id order. The callback
768// returns true to stop, matching avl's own convention.
769func eachOf(issuer address, fn func(*invoice) bool) {
770	prefix := issuer.String() + "|"
771	invoices.Iterate(prefix, prefix+"\xff", func(_ string, v any) bool {
772		return fn(v.(*invoice))
773	})
774}
775
776func getIssuer(key string) *issuerRec {
777	v := issuers.Get(key)
778	if v == nil {
779		return nil
780	}
781	return v.(*issuerRec)
782}
783
784func mustInvoice(issuer address, id int64) *invoice {
785	v := invoices.Get(invKey(issuer, id))
786	if v == nil {
787		panic("unknown invoice")
788	}
789	return v.(*invoice)
790}
791
792// mustDecOpen decrements an issuer's open count by one and REFUSES rather
793// than masking if it cannot.
794//
795// Both call sites — settlement in Pay and forgiveness in Void — have already
796// proved the invoice exists and was StatusOpen, so neither branch below can
797// fire. The decrement is reachable only through a status transition out of
798// StatusOpen, each invoice makes that transition at most once (both guards
799// require StatusOpen and both set a terminal status), and Issue increments
800// `open` for every record it stores. So an open invoice contributed exactly
801// one to this counter and has not yet taken it back.
802//
803// The earlier spelling was `if r := getIssuer(...); r != nil && r.open > 0`,
804// which reads as prudence and is the opposite. Those clauses cannot prevent
805// the inconsistency — the status has already been written by the time we get
806// here — they can only decline to notice it, and then COMMIT it. A panic
807// aborts the whole transaction and reverts the status write too, which
808// leaves the realm consistent; masking leaves it inconsistent and silent,
809// and the counter it corrupts is the one MaxRecordsPerIssuer reads.
810//
811// So the guard exists for a future edit, not for today: a path that retires
812// an invoice without coming through here fails loudly on the next one
813// instead of quietly leaking a slot. A check that can never fire and a
814// check that always passes are the same artifact from a reader's seat; the
815// difference is what each does when it is wrong.
816func mustDecOpen(issuer address) {
817	r := getIssuer(issuer.String())
818	if r == nil || r.open == 0 {
819		panic("invoices: issuer open count desynchronized from the invoice tree")
820	}
821	r.open--
822}
823
824func checkedAdd(a, b int64) (int64, bool) {
825	c := a + b
826	if (b > 0 && c < a) || (b < 0 && c > a) {
827		return 0, false
828	}
829	return c, true
830}
831
832func itoa(n int64) string { return strconv.FormatInt(n, 10) }
833
834// --- render ---
835
836func Render(path string) string {
837	var b strings.Builder
838	b.WriteString("# invoices\n\n")
839	b.WriteString("On-chain accounts receivable. An invoice is unfunded at ")
840	b.WriteString("creation: the obligation is recorded first, and the ")
841	b.WriteString("money may never arrive.\n\n")
842
843	if path == "" {
844		b.WriteString("## Totals\n\n")
845		b.WriteString("| field | value |\n|---|---|\n")
846		b.WriteString("| issuers | " + itoa(int64(Issuers())) + " |\n")
847		b.WriteString("| stored records | " + itoa(int64(Records())) + " |\n")
848		b.WriteString("| settled, unclaimed | " + itoa(SettledTotal()) + " " + Denom + " |\n")
849		b.WriteString("| fees accrued | " + itoa(FeesAccrued()) + " " + Denom + " |\n")
850		b.WriteString("| held | " + itoa(Held()) + " " + Denom + " |\n")
851		b.WriteString("| surplus | " + itoa(Surplus()) + " " + Denom + " |\n\n")
852		b.WriteString("> Anyone may issue an invoice naming anyone, so an ")
853		b.WriteString("unpaid invoice is an unverified claim by its issuer. ")
854		b.WriteString("There is deliberately no global \"what does this ")
855		b.WriteString("address owe\" total; read OutstandingFrom, which is ")
856		b.WriteString("issuer-scoped.\n\n")
857		b.WriteString("Append an issuer address to this path to see their book.\n")
858		return b.String()
859	}
860
861	who := sanitize.InlineText(path)
862	b.WriteString("## Book of " + who + "\n\n")
863	r := getIssuer(path)
864	if r == nil {
865		b.WriteString("No issuer record at that address.\n")
866		return b.String()
867	}
868	b.WriteString("Open: " + itoa(int64(r.open)) + ", ")
869	b.WriteString("stored: " + itoa(int64(r.records)) + "/" +
870		itoa(int64(MaxRecordsPerIssuer)) + ", ")
871	b.WriteString("issued all time: " + itoa(r.issued) + ", ")
872	b.WriteString("face value all time: " + itoa(r.invoiced) + " " + Denom + "\n\n")
873
874	now := runtime.ChainHeight()
875	b.WriteString("| id | payer | amount | paid | due | status | memo |\n")
876	b.WriteString("|---|---|---|---|---|---|---|\n")
877	prefix := path + "|"
878	invoices.Iterate(prefix, prefix+"\xff", func(_ string, v any) bool {
879		inv := v.(*invoice)
880		state := inv.status
881		if isOverdue(inv, now) {
882			state = "OVERDUE"
883		}
884		b.WriteString("| " + itoa(inv.id))
885		b.WriteString(" | " + sanitize.TableCell(inv.payer.String()))
886		b.WriteString(" | " + itoa(inv.amount))
887		b.WriteString(" | " + itoa(inv.paid))
888		b.WriteString(" | " + itoa(inv.dueHeight))
889		b.WriteString(" | " + state)
890		b.WriteString(" | " + cell(inv.memo) + " |\n")
891		return false
892	})
893	return b.String()
894}
895
896// cell prepares caller-supplied text for a GFM table cell, truncating it
897// for display first.
898//
899// TableCell rather than InlineText, and the slot is the reason: these
900// bytes land between two `|` delimiters, and an unescaped `|` in a memo
901// would open a new column and shift every column to its right. InlineText
902// does not escape the pipe. The payer cell above uses the same helper for
903// the same slot even though a bech32 address cannot contain one — matching
904// the helper to the slot is the rule that survives a later edit, whereas
905// "this particular value happens to be safe" is a fact about today's
906// charset.
907//
908// TRUNCATE BEFORE ESCAPING, and on a RUNE boundary. Escaping first and
909// cutting afterwards can slice a `\|` in half and leave the cell ending in
910// a bare backslash; cutting on a byte boundary can split a multi-byte rune
911// and emit invalid UTF-8. Neither is something the sanitizer is asked to
912// repair, because by the time it sees the string the damage is upstream.
913func cell(s string) string {
914	const maxRunes = 48
915	if r := []rune(s); len(r) > maxRunes {
916		s = string(r[:maxRunes]) + "…"
917	}
918	return sanitize.TableCell(s)
919}