Cosmic Bull

latefees

gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees on onyx-1 testnet

realmapplicationreceivableslive on onyx-1

Charge a late fee that grows the longer an invoice stays unpaid.

The creditor publishes the schedule once, so the penalty on an overdue invoice can be read off instead of argued over.

In detail curated

A GRADUATED LATE-FEE PUBLISHER over the live r/invoices, and the portfolio's first realm to IMPORT ANOTHER REALM through the deploy path (invoices, read by scalar view). A creditor who issued invoices declares a p/tiers schedule over OVERDUE BLOCKS; the realm prices a penalty as a function of overdue DURATION, escalating by tier, readable live (PenaltyNow) or frozen into a sequence-numbered assessment (Assess). It is the fourth consumer of p/tiers, exercising its progressive-fee reading over TIME where r/meters prices usage and r/payslips withholding; and the second consumer of r/invoices after r/receivables, reading the integration surface that realm's header advertises -- InvoiceInfo, IsOverdue, HasInvoice, all non-crossing, so no struct slice crosses a realm boundary. IT IS A PUBLISHER, NOT A COLLECTOR: r/invoices is immutable so this realm cannot add a charge there, and it holds NO coins -- no banker, no deposit, no payout -- so the threat model is accounting integrity and authorization, not custody, and the monetary-conservation invariant does not apply. A published penalty is a verifiable CLAIM backed by the invoice's own facts and the creditor's declared schedule, exactly as receivables publishes a loss provision; it compels nothing. THE PRICING AXIS is overdue duration, not balance: p/tiers prices one amount against breakpoints (one dimension), and the base is now - dueHeight with each tier's rate a ugnot accrual per 10000 blocks, so penalty <= overdueBlocks by the rate cap -- a time-escalating absolute accrual, NOT a percentage of the outstanding (two dimensions p/tiers cannot express in one call), with the outstanding recorded beside the penalty so a reader can derive an effective rate. AUTHORIZATION is the invoices key scoping: Assess derives the issuer from cur.Previous().Address() and reads InvoiceInfo under invKey(issuer,id), which returns only invoices that address issued, so a creditor may assess only debts owed TO them -- proven live, a foreign caller reads 'no invoice ... issued by the caller'. THE TRUST BOUNDARY is a self-report of a self-report of a self-report: the issuer set the invoice amount, its due height, and this schedule; the realm computes faithfully and validates none of them for fairness. PenaltyNow reads 0 once the invoice is no longer overdue (paid or voided), because r/invoices does not expose the pay height, so accrual is computable only while open and late -- a frozen Assess is how a claim survives payment.

Use it

A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.

Read it — no key, no account, no GNOT

gnokey query vm/qeval \
  --data 'gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees.Height()' \
  --remote https://rpc.onyx.testnets.gno.land:443

Height is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.

Change its state

Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 2 crossing functions, with the argument names filled in.

Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.

Import it from another realm

import "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Deployments

onyx-1 testnet onyx-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height483,001
Deploy transactionc56bd70be9a0eee15443af38f1c0206cde58da4bed8044b297f85cbbf28823b3 look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used24,254,154
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml latefees.gno
Deployed byteslatefees.gno — 17,007 bytes
sha2564ea5efcd1546366cf1408b8ac4b6f5b6f6882d76572fdfb51fb1b791ad35d5ce
Realm addressg1mwkdz0e3ay3s3syuwvngcmn5q6rm8tgpsz48de

Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/latefees$download&file=latefees.gno' | shasum -a 256

Expected: 4ea5efcd1546366cf1408b8ac4b6f5b6f6882d76572fdfb51fb1b791ad35d5ce — 17,007 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 13 exported functions.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AssessAssessmentScheduleAssessmentsOfHasAssessmentHasScheduleHeightInvoicesNumAssessmentsPenaltyNowQuotePenaltyRenderScheduleOfSetSchedule

Overview

Realm latefees publishes a graduated late-fee penalty on an overdue invoice held in the live r/invoices. A creditor (an invoice issuer) declares a late-fee SCHEDULE once — a p/tiers schedule over how many blocks an invoice is overdue — and the realm prices the penalty as a function of that overdue duration, escalating by tier. The penalty can be read live (PenaltyNow) or frozen into a sequence-numbered ASSESSMENT the creditor can point a payer to.

It is the fourth consumer of p/tiers and the one that exercises its "progressive fee" reading over TIME rather than quantity: r/meters prices usage, r/payslips prices withholding, and this prices lateness. It is the second consumer of r/invoices after r/receivables, and reads the exact integration surface that realm's header advertises — invoices.InvoiceInfo, invoices.IsOverdue, invoices.HasInvoice — all non-crossing views, no struct slice crossing a realm boundary.

It is a PUBLISHER, not a collector

r/invoices is live and immutable, so this realm CANNOT add a charge to an invoice there, and it holds no coins of its own — no banker, no deposit, no payout, no withdrawal path. A published penalty is a verifiable CLAIM backed by the invoice's own facts and the creditor's declared schedule, exactly as r/receivables publishes a loss provision and r/cashflow a statement. It compels nothing; a debtor who ignores a late fee is in the same position as one who ignores the invoice. A custodial variant that collects the fee is a separate, larger product and is deliberately out of scope.

The pricing axis, and the one it is not

The base handed to p/tiers is the OVERDUE DURATION in blocks (`now - dueHeight`), and each tier's rate is a ugnot accrual per RateScale blocks. So the penalty ESCALATES WITH TIME: the longer an invoice is overdue, the higher the marginal accrual. p/tiers' rate cap makes `penalty <= overdueBlocks` an arithmetic fact — the fee can never exceed one ugnot per overdue block, a natural ceiling.

It is NOT a percentage of the outstanding amount. p/tiers prices one amount against breakpoints — a single dimension — and a fee that is both time-escalating AND balance-scaled is two dimensions it cannot express in one call. The creditor scales the ugnot accrual to their debt magnitudes through the schedule; the assessment records the outstanding beside the penalty so a reader can compute an effective rate. Composing a balance- scaled fee as a single Charge call would be a plausible, wrong number.

The trust boundary, inherited twice over

A penalty is a self-report of a self-report of a self-report: the issuer set the invoice amount in r/invoices, the due height there, AND the late-fee schedule here. This realm computes faithfully from those; it does not validate that any of them is fair. A creditor may only assess their OWN invoices — Assess derives the issuer from `cur.Previous().Address()` and reads invoices.InvoiceInfo under that key, which returns only invoices that address issued.

Imports

Constants and variables

Denom is the asset the invoices this realm prices are denominated in, and the unit a penalty is expressed in. This realm never holds it.

const Denom = "ugnot"

InvoicesPath pins the realm this one reads, by value, so a reader of a published assessment can see which invoices it priced.

const InvoicesPath = invoices.RealmPath

MaxAssessmentsPerIssuer bounds one creditor's stored assessment records, so one address cannot grow this realm's state without bound. It counts STORED records, the invoices-realm discipline.

const MaxAssessmentsPerIssuer = 256

RenderLimit bounds every rendered list — an unbounded Render was YELLOW in three prior audits.

const RenderLimit = 20

Functions

Assess

func Assess(cur realm, invoiceID int64) int64

Crossing function. Callable from a transaction via MsgCall, and from another realm as Assess(cross(cur), ...).

Assess freezes a late-fee penalty on one of the caller's own overdue invoices and returns its sequence number. The caller must be the invoice's issuer (the creditor), must have declared a schedule, and the invoice must be overdue right now. The invoice's facts and the current schedule are snapshotted, so the assessment is a stable claim even as the invoice keeps accruing or is later paid.

AssessmentSchedule

func AssessmentSchedule(seq int64) (edges, rates []int64)

AssessmentSchedule returns the schedule snapshot an assessment was priced under — the edges and rates frozen with it, which may differ from the issuer's current schedule.

AssessmentsOf

func AssessmentsOf(issuer address) int64

AssessmentsOf is the number of assessments an issuer has frozen.

HasAssessment

func HasAssessment(seq int64) bool

HasAssessment reports whether a sequence number names a frozen assessment.

HasSchedule

func HasSchedule(issuer address) bool

HasSchedule reports whether an issuer has declared a late-fee schedule.

Height

func Height() int64

Invoices

func Invoices() string

NumAssessments

func NumAssessments() int64

PenaltyNow

func PenaltyNow(issuer address, invoiceID int64) int64

PenaltyNow is the penalty an issuer's invoice has accrued as of the current height, under the issuer's CURRENT schedule. It is 0 when the invoice is not overdue — including once it is paid or voided, because r/invoices does not expose when a payment landed, so accrual is only computable while the invoice is still open and late. A frozen Assess is how a claim survives the invoice being paid. Panics only if the issuer has no schedule or the invoice does not exist.

QuotePenalty

func QuotePenalty(issuer address, overdueBlocks int64) int64

QuotePenalty prices a hypothetical overdue duration against an issuer's current schedule, without reading any invoice — the preview a creditor uses to tune a schedule. Panics if the issuer has no schedule or overdueBlocks is negative.

Render

func Render(path string) string

Render shows the catalog at "", an assessment at "seq/<n>", and an issuer's schedule at "issuer/<addr>". Every user-supplied value rendered between table pipes is escaped with sanitize.TableCell; addresses carry no markdown metacharacter but are passed through InlineText for the same reason the sibling realms do.

ScheduleOf

func ScheduleOf(issuer address) (edges, rates []int64)

ScheduleOf returns an issuer's current schedule as its edge and rate slices, or two empty slices if none is declared.

SetSchedule

func SetSchedule(cur realm, edges, rates string) int

Crossing function. Callable from a transaction via MsgCall, and from another realm as SetSchedule(cross(cur), ...).

SetSchedule declares or replaces the caller's late-fee schedule: a graduated accrual over OVERDUE BLOCKS, built from comma-separated int64 lists where len(rates) == len(edges)+1 and each rate is a ugnot accrual per RateScale blocks (0..10000). Edges are block counts — e.g. "216000,648000" is roughly ten and thirty days at ~4s blocks. A single rate with no edges is a flat accrual. Replacing a schedule does not touch already-frozen assessments, which snapshot the schedule that priced them.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importschain, chain/runtime, chain/runtime/unsafe, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiers, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices, strconv
First-party dependenciesgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/tiers, gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/invoices

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source fileonyx/r/latefees/latefees.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical