Cosmic Bull

reimbursements

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

realmapplicationdisbursementlive on onyx-1

Let people claim expenses back, with an approver who says yes or no.

The claimant names an amount already spent, and approval pays it out in the same call, so nobody can withdraw the money out from under an approved claim.

In detail curated

CLAIMANT-PRICED, APPROVER-GATED EXPENSE REIMBURSEMENT against a shared UN-ESCROWED pool. Request-then-approve is NOT new in this portfolio -- grants, bounties and bounty_panel all let a requester initiate and a second party gate before value moves, and an earlier draft of the realm's own doc comment wrongly claimed novelty; DISCOVERY.md records the sweep that found them. Three narrower claims carry this realm instead, all load-bearing. THE REQUESTER PRICES IT: a grant or bounty amount is the funder's offer, fixed before anyone asks, while a reimbursement amount is a cost the claimant already incurred, so the claimant names the figure and the approver's only lever is yes or no on it. SETTLEMENT IS NON-EXCLUSIVE: those three are competitive awards with per-item escrow where exactly one requester wins, whereas here an unbounded number of requests settle against ONE SHARED, UN-ESCROWED pool -- so a pending request is NOT a reservation, the divergence the realm is built on and the first thing proven live below. THE APPROVER IS RATE-LIMITED: not the owner, holding a bounded, windowed, revocable spendbook authority, so an approver key cannot drain the pool. It is also spendbook's SECOND CONSUMER and the first with an INVERTED temporal order -- budgets consumes authority at spend time by the spender; here the spendbook spender is the APPROVER and the authority is consumed at APPROVE time against a request a third party submitted. A second consumer that uses a primitive the same way proves little more than one; this one exercises the opposite direction. CUSTODY: Held == TotalPooled + feeledger.Liabilities(), settlement is PULL (Approve moves pool -> claimant credit inside the realm and the claimant withdraws separately), the approval fee is 100 bps snapshotted per desk under a MaxFeeBps=500 compile-time ceiling, and every request carries a MANDATORY RequestTTL=86,400-block expiry with a permissionless ExpireRequest. Audit PROCEED: 0 RED, one YELLOW DECLARED rather than fixed (Y1 -- a queued widening expiry amendment is lost because spendbook's IsExpired bypasses at(); fail-closed, no fund loss, unfixable after addpkg, and the mechanism was EXECUTED on live onyx-1 rather than reasoned about). 37-test suite green, 60-case mutation sweep FULLY COVERED. BYTE-MATCH PASSED on the first attempt, and that is this deploy's second result: it is the first deploy over gnomcp #107's package_dir path, and #107's first live proof after r/deposits' inline emitter re-wrapped a comment and broke a byte-match permanently.

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/reimbursements.FeesAccrued()' \
  --remote https://rpc.onyx.testnets.gno.land:443

FeesAccrued 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 14 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/reimbursements"

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/reimbursements
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height551,862
Deploy transaction0998f838ab6934d0e30b9929143e0e7daae1aaed48e698bfd7ebf8aece2c37cd look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used92,927,889
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml reimbursements.gno
Deployed bytesreimbursements.gno — 70,778 bytes
sha25600160c28439e4dbf7bb3544d280b68c28c08a87b905672bb72b004e1dafb1096
Realm addressg1dmk5au70rtqlc2qnz9zsqc0jfc58t056ds7sew

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/reimbursements$download&file=reimbursements.gno' | shasum -a 256

Expected: 00160c28439e4dbf7bb3544d280b68c28c08a87b905672bb72b004e1dafb1096 — 70,778 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 41 exported functions.

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

AmendDeskApprovableApprovalBlockerApproveClaimCloseDeskCreditOfDeskLabelDeskUsageDesksOfExpireRequestFeesAccruedFundHeightHeldIsDeskExpiredIsRequestExpiredLiabilitiesLiveDesksLiveRequestsNumDesksNumRequestsOpenDeskOpenRequestsAtOpenRequestsOfPendingTermsPoolOfReapDeskRejectRemainingRenderRequestMemoRetractSubmitSurplusSweepSurplusTotalPooledUnclaimedCreditWithdrawFeesWithdrawPoolWithdrawPoolAll

Overview

Realm reimbursements is a claimant-initiated, approver-gated expense reimbursement desk in GNOT: an owner funds a pool and appoints an approver with a bounded, rate-limited, revocable authority to approve against it; anyone may then SUBMIT a request for a specific amount with a memo; and the approver either approves it — which settles the money into the claimant's credit immediately — or rejects it.

WHY THIS EXISTS (see DISCOVERY.md): request-then-approve is NOT new here, and an earlier draft of this comment wrongly claimed it was. Three realms in this portfolio already let a requester initiate and a second party gate before value moves — grants (applicants apply, the creator selects), bounties (a funder designates a winner) and bounty_panel (contributors submit, an M-of-N panel votes). The §7 sweep that found them is recorded in DISCOVERY.md. The claim this realm actually rests on is narrower, and all three parts are load-bearing:

The four other disbursement shapes here are further away still: treasury_board needs the PROPOSER to be a signer, so an outside claimant cannot ask; budgets grants authority up front with no per-item gate; invoices is a receivable nobody approves; payroll and vesting are employer-funded accrual on a schedule.

It is also, deliberately, the SECOND CONSUMER of spendbook, and the first with an INVERTED temporal order. budgets consumes authority at spend time by the spender. Here the spendbook "spender" is the APPROVER, and the authority is consumed at APPROVE time against a request a third party submitted. A primitive with one consumer is barely proven as a primitive; a second consumer that uses it the same way proves little more. This one exercises the opposite direction.

COMPOSITION: approval authority is delegated entirely to spendbook (anchored windows, lifetime caps, expiry, one-sided amendments, revocation), coin movement to coinio, per-claimant credit and the fee pot to feeledger, ordered storage to p/nt/avl/v0, and free-text render output to p/nt/markdown/sanitize/v0. This realm owns the pool accounting, the desk and request records, the conflict-of-interest rule, and the solvency rule.

A desk, and what an approval IS

A desk is one (owner, approver) pair with a bounded authority over the owner's pool. A request is one claimant's ask against one desk.

The DELIBERATE DIVERGENCE from budgets is at approval:

An approved reimbursement is a SETTLED OBLIGATION, not authority.

Approve moves the gross out of the owner's pool and into the claimant's ledger credit in the same call. The owner cannot then withdraw it out from under an approved claimant, and no second step can fail. budgets' rule — "a budget is authority, not a reservation" — is explicitly NOT inherited, because the two realms are answering different questions. A budget is an ongoing permission whose point is not pre-committing capital. An approved expense claim is a decision about a cost already incurred, and a decision that can evaporate is not a decision.

The realm therefore never holds a partially-committed state: a request is pending (nothing has moved) or settled (everything has moved).

A PENDING REQUEST IS NOT A LIABILITY

Stated plainly because the opposite is the natural assumption. Submitting reserves nothing, locks nothing, and is not a claim on the pool. Ten pending requests of 100 against a pool of 100 are all ten submittable; the approver can approve one of them. Liabilities() counts pooled funds and settled credit, and a pending request appears in neither. What a claimant can rely on is the moment of approval, which is atomic, and the Approvable view, which reports the honest figure before they are told yes.

THE APPROVER IS THE SPENDBOOK SPENDER

The desk's allowance is consumed by the GROSS approved amount, so it bounds the owner's total OUTFLOW per window rather than the claimant's receipt — the figure an owner actually cares about capping, and the same treatment budgets gives its fee. An approver with a PerPeriod of 1 GNOT can approve 1 GNOT of the owner's money per window no matter how many claimants ask, and no single compromised approver key can do more than its window allows before the owner revokes the desk.

SELF-APPROVAL IS PROHIBITED

Approve refuses when the request's claimant is the approving address. This is a real conflict-of-interest control and not a formality: an approver with a desk could otherwise reimburse themselves at will up to the window cap. The rule is per-APPROVAL, not per-desk, so an address cannot route its own claim through a second desk it also approves. An approver may still submit requests — to be approved by somebody else.

LIFECYCLE

Fund (owner, +send) : adds real coins to the caller's own pool. Anyone may fund; funding makes you an owner. Minimum MinFunding. WithdrawPool (owner) : pulls UNSPENT pool back out, any amount, any time. Approved credit is already out of the pool and unreachable. WithdrawPoolAll (owner) : the whole unspent pool. OpenDesk (owner) : appoints one approver with bounded authority and returns the desk id. The approval fee is snapshotted here. AmendDesk (owner) : changes the desk's terms. Tighter-or-equal applies now; looser waits for the next window boundary. periodLength is NOT a parameter — spendbook makes it immutable. CloseDesk (owner) : immediate; the id is never reused. ReapDesk (ANYONE) : removes an EXPIRED desk, freeing its slot. Submit (ANYONE) : files a request against a desk for a specific amount with a memo, and returns the request id. Expires after RequestTTL blocks. Approve (APPROVER) : settles the request — debits the owner's pool, credits the claimant net of the fee, consumes the desk's authority, removes the request. Reject (APPROVER) : closes the request with a reason. Nothing moves. Allowed even after expiry. Retract (CLAIMANT) : the claimant withdraws their own request. Nothing moves. Named Retract, not Withdraw, because every Withdraw* in this portfolio moves money and this moves none. ExpireRequest (ANYONE) : removes a request past its expiry. Claim (CLAIMANT) : pays the claimant's whole settled credit out. The portfolio's standard pull. WithdrawFees (ANYONE) : pays the fee pot to the compile-time FeeCollector. Fixed destination. SweepSurplus (ANYONE) : out-of-band coins to the FeeCollector, never touching tracked liabilities.

A CLOSED DESK LEAVES ITS REQUESTS UN-APPROVABLE

CloseDesk and ReapDesk remove the desk and its allowance. Requests filed against it remain as records and can never be approved — Approve panics on the missing desk. They can be Retracted by their claimant or, once past expiry, ExpireRequested by anyone.

There is deliberately NO CASCADE DELETE. Finding a closed desk's requests means iterating the request tree, which is unbounded work in a transaction whose gas the owner pays, and an owner who cannot close a desk because it has too many pending requests is a worse realm than one where closing leaves records to be swept. The condition is documented, visible in RequestInfo and on the request's render page, and self-clearing within RequestTTL.

AVAILABILITY BOUND, stated honestly

Submit is permissionless, so the request tree is fillable. The per-claimant and per-desk caps bound ONE address and ONE desk, and are defeated by many addresses — that is the correct way to read a per-identity cap on a permissionless chain, and budgets' own corrected comment (finding Y1) is this portfolio's record of getting it wrong the other way.

What genuinely helps here, and does not help in budgets, is that a request has a MANDATORY expiry: RequestTTL is a compile-time constant, every request carries submittedAt + RequestTTL, and ExpireRequest is permissionless with the freed storage deposit refunded to the sweeping transaction. So the fill is bounded in DURATION as well as priced in gas and storage, and it drains itself. budgets' grants with expiresAt == 0 can never be reaped by anyone, which is strictly worse. The residual is real: for RequestTTL blocks a filled tree blocks NEW requests. Every desk, pool, approval, credit and claim keeps working, and no value is at risk.

THERE IS NO ADMIN

The fee rate, the fee collector, the TTL, every bound and every cap are compile-time constants. No address can change terms, pause the realm, touch a pool it did not fund, approve at a desk it was not appointed to, or move a request it did not file. The deployer has no privilege of any kind after deployment.

MONETARY INVARIANT (conservation)

Let H be the ugnot held at this realm's address, P the sum of all owner pools (tracked O(1) as totalPooled), L the ledger's liabilities (settled claimant credit plus the fee pot), and S >= 0 the out-of-band surplus:

H == P + L + S

Every transition moves value between exactly two terms inside one transaction. Fund raises H and P together (coinio.Receive is the receipt-guaranteed shape). WithdrawPool debits P before the identical amount leaves H. Approve moves the gross out of P and into L, split into the claimant's credit and the fee — H does not move at all, which is why Approve has no payout to fail. Claim and WithdrawFees debit L before their payout. Submit, Reject, Retract and ExpireRequest move nothing in any term. Any panic aborts the whole transaction; this realm never issues or removes coins.

ONLY GNOT IS ACCEPTED

Fund rejects any envelope that is not exactly one positive ugnot coin (coinio.Receive). Every other entrypoint rejects attached coins outright rather than converting them into sweepable surplus.

AUTHORIZATION

Every identity is derived from the crossing entrypoint's cur.Previous().Address(). No function takes a caller identity as a parameter, so funding another owner's pool, approving at another address's desk, retracting another claimant's request, or claiming another claimant's credit is impossible by construction. This is spendbook consumer contract item 2, closed structurally.

The clock is runtime.ChainHeight(), never an argument — spendbook consumer contract item 1. A caller who could choose `now` could roll the approval window at will, and could also expire a request out from under its claimant or keep an expired one alive.

STORAGE

A pool entry is paid for by the owner's own funding transaction; a desk record and its allowance by the owner's OpenDesk; a request record by the claimant's Submit. CloseDesk, ReapDesk, Approve, Reject, Retract and ExpireRequest each remove a record, and the chain refunds the freed storage deposit to the transaction that frees it — which is the standing incentive to call ExpireRequest or ReapDesk on somebody else's stale record.

Imports

Constants and variables

ApprovalFeeBps is the fee actually snapshotted into every desk: 100 bps = 1%, charged once per approval on the gross amount, floor-rounded in the claimant's favor. The most conservative live fee in this portfolio (fee_split, crowdfund, budgets) is also 1%.

const ApprovalFeeBps = int64(100)

Denom is the only asset this realm holds.

const Denom = "ugnot"

FeeCollector receives withdrawn fees and swept surplus. Compile-time constant: there is no setter and no transfer path. It is the deploying namespace's address — stated plainly: the operator of this realm.

const FeeCollector = address("g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3")

MaxDesksPerOwner bounds how much of the global desk cap one owner can occupy. spendbook's package documentation names this as the gap a consuming realm must close: "maxLive is a single global figure, so any owner permitted to Open can exhaust it and lock out the rest … a Book shared across mutually untrusting owners needs its own per-owner cap on top of this one." This is that cap.

const MaxDesksPerOwner = int64(16)

MaxFeeBps is the hard ceiling on any approval fee this realm could ever charge, enforced by the feeledger the realm is constructed with — a feeBps above it is refused by the primitive's own validation, not by a check in this file. 500 bps = 5%.

const MaxFeeBps = int64(500)

MaxLabelLen bounds a desk's free-text label.

const MaxLabelLen = 64

MaxLiveDesks caps simultaneously live desks, via spendbook's own construction bound. OpenDesk requires a funded pool, which is NOT a capital bound — see MaxOpenPerClaimant for why that mistake is not repeated here. spendbook's own MaxLiveLimit is 10000.

const MaxLiveDesks = 4096

MaxLiveRequests caps simultaneously live requests across the realm. Submit is permissionless, so this cap is exhaustible, and the package doc states that bound rather than claiming the per-identity caps below close it.

const MaxLiveRequests = 4096

MaxMemoLen bounds a request's free-text memo and a rejection's reason. Larger than MaxLabelLen because a memo is the expense description — the thing an approver reads to decide — while a label only names a desk.

const MaxMemoLen = 160

MaxOpenPerClaimant bounds one claimant's simultaneously-pending requests. It is a GRIEF-COST multiplier and not a defence: one address is bounded, N addresses are not, and an attacker controls N. The real mitigation is RequestTTL plus permissionless ExpireRequest — see the package doc's availability section.

const MaxOpenPerClaimant = int64(16)

MaxOpenPerDesk bounds the pending requests at one desk, so a single desk's approver is not handed an unreviewable queue by one burst. It is per-desk, so filling it denies that desk only; other desks are unaffected.

const MaxOpenPerDesk = int64(64)

MaxRenderRows bounds the lists on the index page.

const MaxRenderRows = 20

MinFunding keeps dust pools out: 1 GNOT per Fund call.

const MinFunding = int64(1_000_000)

MinPerPeriod is the smallest per-window approval cap a desk may carry. It exists so a desk cannot be opened that no approval could ever satisfy, and it is paired with MinRequestAmount below rather than standing alone.

const MinPerPeriod = int64(1_000)

MinPeriodBlocks is the shortest window a desk may use. A window of a handful of blocks is a rate, not a budget, and it makes PerPeriod meaningless as a bound on worst-case loss. A periodLength of 0 is still accepted and means ONE LIFETIME WINDOW that never refills (spendbook's own semantics).

const MinPeriodBlocks = int64(100)

MinRequestAmount is the smallest request that may be filed, and it is deliberately EQUAL TO MinPerPeriod rather than smaller. The two together are what make a desk's authority expressible: a desk whose per-window cap is at the floor can still approve a request at the floor. Setting this below MinPerPeriod would admit dust requests that cost the approver a transaction each; setting it above would create desks no request could use.

const MinRequestAmount = int64(1_000)

RealmPath is this realm's own path, used to build Render links.

const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/reimbursements"

RequestTTL is how long a request stays approvable, IN BLOCKS. No wall-clock equivalent is stated because onyx-1's block time has never been measured by this project, and converting blocks to hours with an assumed rate is how a document comes to carry a number nobody checked. It is sized to be long enough that an approver need not be online continuously and short enough that the request tree drains.

const RequestTTL = int64(86_400)

Functions

AmendDesk

func AmendDesk(cur realm, id int64, perPeriod, total, expiresAt int64) bool

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

AmendDesk changes a desk's terms. Owner only. The returned bool reports whether the new terms are in force NOW (a tighter-or-equal amendment) or queued for the next window boundary (a looser one) — spendbook's one-sided rule, and the distinction worth knowing.

periodLength is NOT a parameter. spendbook makes it immutable because moving it would move every future boundary, which is an enlargement vector wearing a different hat; this realm reads the desk's own periodLength and passes it unchanged, so changing it is not merely refused, it is unexpressible.

Amending does not touch pending requests. Their fee was snapshotted at Submit and their amount is fixed; what changes is whether the approver still has the authority to settle them, which Approvable reports.

A QUEUED EXPIRY EXTENSION CAN BE LOST, AND THE DESK BECOMES REAPABLE IN THE MEANTIME. A widening amendment is queued rather than applied, and spendbook surfaces a queued term only through its internal at() accessor — which IsExpired does NOT use: it reads the live Terms.ExpiresAt directly. So between queueing and the next approval, IsDeskExpired and ReapDesk still read the OLD expiry. If the old expiry passes with no Approve and no restated AmendDesk in between, ANYONE may ReapDesk the desk and the queued extension dies with it. Restating the same amendment while the old expiry is still in the future makes it permanent, because spendbook writes Pending back to Terms on a spend or on a later amendment; restating it after the old expiry is refused with "allowance has expired".

NO FUNDS ARE AT RISK and that is structural, not incidental: dropDesk moves no coins, and the claimant pools are keyed by CLAIMANT ADDRESS rather than by desk, so a reap strands open requests and leaves every accrued balance claimable. The root cause is in spendbook, which is already live and immutable, so the repair belongs to a future /p/ version and not to this realm. See AUDIT.md Y1 and DEPLOY_READINESS.md.

Approvable

func Approvable(id int64) int64

Approvable is the HONEST figure for a desk, and the one both an approver and a prospective claimant should read: the lesser of what the desk authorizes and what the owner's pool holds. This distinction is the realm's own — spendbook bounds permission and says so, never solvency.

ApprovalBlocker

func ApprovalBlocker(id int64) string

ApprovalBlocker returns the empty string when the request could be approved right now by its desk's approver, and otherwise the single reason it could not. It is the view a UI wants before showing an approve button, and an approver wants before paying gas for a call that will abort.

The reasons are reported in the same order Approve checks them, so this view and that entrypoint can never disagree about WHICH reason applies. It deliberately does NOT report "you are not the approver": the caller of a view is not the caller of the approval, so this answers about the request and the desk, not about whoever is asking.

Approve

func Approve(cur realm, reqID int64) int64

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

Approve settles a request. APPROVER ONLY: the approver is the calling address, never an argument. The claimant is credited net of the fee and claims separately, which is this portfolio's standard pull shape.

The checks fail in this order, and the order matters:

  1. the desk must still exist (a closed or reaped desk strands its requests, by design);
  2. the caller must be that desk's approver;
  3. the caller must not be the request's own claimant (no self-approval);
  4. the request must not have expired;
  5. the owner's pool must cover the GROSS amount;
  6. the desk's spendbook authority must permit the gross amount.

Solvency (5) is checked BEFORE the allowance is consumed (6), so an approval refused for insolvency leaves the desk's window untouched rather than burning capacity on an approval that did not happen. This is budgets' ordering, kept for the same reason.

Claim

func Claim(cur realm) int64

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

Claim pays the caller's entire settled credit out. CLAIMANT ONLY: the account is the calling address. Credit from a desk that has since been closed, expired or reaped is still claimable — it was settled under a live authority and this realm does not revisit that.

CloseDesk

func CloseDesk(cur realm, id int64)

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

CloseDesk ends an approver's authority immediately. Owner only. The id is never reused, so a closed desk cannot be resurrected.

Closing does NOT claw back credit already approved — that is the claimant's and stays claimable. It does NOT remove requests filed at this desk either: see the package doc, "a closed desk leaves its requests un-approvable". The count of those is reported in the event so the owner learns what they just stranded.

CreditOf

func CreditOf(addr address) int64

CreditOf returns a claimant's settled-but-unclaimed credit.

DeskLabel

func DeskLabel(id int64) string

DeskLabel returns a desk's raw label (unsanitized: callers rendering it into markdown must sanitize, as this realm's Render does).

DeskUsage

func DeskUsage(id int64) (approvedWindow, approvedTotal, windowIndex,
	approvals, rejections, pending int64)

DeskUsage returns a desk's consumption AT THE CURRENT HEIGHT: what the window the chain is in right now has approved, the lifetime gross, that window's index, how many approvals there have been, how many rejections, and how many requests are pending at the desk.

The two window figures are computed rather than read straight off the record, and that is the whole point of windowAt below: a window that has rolled with no approval since leaves Allowance.WindowIndex naming a window the chain has already left, and Allowance.SpentWindow holding the previous window's spend.

DesksOf

func DesksOf(addr address) int64

DesksOf returns how many live desks an owner currently holds.

ExpireRequest

func ExpireRequest(cur realm, reqID int64)

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

ExpireRequest removes a request past its expiry, freeing its slot and its storage. ANYONE may call it, and the reasoning matches ReapDesk's: an expired request can never be approved, so removing it takes nothing from the claimant and grants nothing to the caller. The chain refunds the freed storage deposit to the expiring transaction, which is the standing incentive and the mechanism that drains the queue.

FeesAccrued

func FeesAccrued() int64

FeesAccrued returns the fee pot: charged, not yet withdrawn.

Fund

func Fund(cur realm) int64

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

Fund adds the attached coins to the caller's own pool. Direct EOA calls with -send only (the receipt-guaranteed shape); exactly one positive ugnot coin, at least MinFunding. Anyone may fund; funding is what makes an address an owner.

Height

func Height() int64

Height returns the current chain height: the clock every window, expiry and TTL is measured against, and the one no caller can choose.

Held

func Held() int64

Held returns H: the ugnot actually at this realm's address.

IsDeskExpired

func IsDeskExpired(id int64) bool

IsDeskExpired reports whether a desk's authority has lapsed, which is also the condition under which anyone may ReapDesk it.

It reads the LIVE expiry only. A queued expiry extension reported by PendingTerms is NOT considered here, so this can read true while an amendment widening the window is still queued — see AmendDesk for the full disclosure and why no funds are at risk.

IsRequestExpired

func IsRequestExpired(id int64) bool

IsRequestExpired reports whether a request is past its expiry, which is both the condition under which Approve refuses and the condition under which anyone may ExpireRequest it.

Liabilities

func Liabilities() int64

Liabilities returns P + L — everything this realm owes. A pending request is not part of it.

LiveDesks

func LiveDesks() int64

LiveDesks returns how many desks are currently live, from the primitive's own count.

LiveRequests

func LiveRequests() int64

LiveRequests returns how many requests are currently pending. A pending request may still be unapprovable — see ApprovalBlocker.

NumDesks

func NumDesks() int64

NumDesks returns how many desks have ever been opened. Ids are never reused, so this only grows.

NumRequests

func NumRequests() int64

NumRequests returns how many requests have ever been filed. Ids are never reused, so this only grows.

OpenDesk

func OpenDesk(cur realm, approver address, label string,
	perPeriod, periodLength, total, expiresAt int64) int64

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

OpenDesk appoints an approver with a bounded authority over the caller's pool and returns the desk id. The caller becomes the desk's owner and must already have a funded pool — not because the desk reserves anything (it does not), but because a desk over an unfunded pool is an invitation to claimants to file requests nothing can settle, and this realm declines to record one.

Terms, all in the chain's own units:

perPeriod the gross the approver may approve per window, at least MinPerPeriod. Required. periodLength blocks per window. 0 means ONE LIFETIME WINDOW that never refills; otherwise at least MinPeriodBlocks. total lifetime cap across every window. 0 means unlimited. expiresAt absolute block height at which the approver's authority ends. 0 means no expiry.

Every bound above except the two realm minimums is validated and enforced by spendbook, and an invalid combination surfaces as that package's own sentinel error.

OpenRequestsAt

func OpenRequestsAt(deskID int64) int64

OpenRequestsAt returns how many pending requests a desk has.

OpenRequestsOf

func OpenRequestsOf(addr address) int64

OpenRequestsOf returns how many pending requests a claimant has.

PendingTerms

func PendingTerms(id int64) (perPeriod, total, expiresAt int64, pending bool)

PendingTerms returns a queued loosening amendment on a desk and whether there is one. An amendment is queued only when it would widen the live window, and it takes effect at the next window boundary.

PoolOf

func PoolOf(addr address) int64

PoolOf returns an owner's unspent pool.

ReapDesk

func ReapDesk(cur realm, id int64)

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

ReapDesk removes a desk whose authority has EXPIRED, freeing its slot and its storage. Anyone may call it, and the reasoning is spendbook's own: an expired allowance can never authorize another approval, so removing it takes nothing from the owner and grants nothing to the caller. The chain refunds the freed storage deposit to the reaping transaction, which is the standing incentive.

A desk with no expiry can never be reaped; only its owner can close it.

Reject

func Reject(cur realm, reqID int64, reason string)

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

Reject closes a request without settling it. APPROVER ONLY. Nothing moves in any term of the invariant, and no authority is consumed — a rejection costs the desk's window nothing, which is deliberate: an approver must be able to say no as often as necessary.

Rejecting an EXPIRED request is allowed. The alternative would force an approver who wants the record to carry a reason to race the expiry, and the reason is the useful artifact: ExpireRequest emits no explanation.

Remaining

func Remaining(id int64) int64

Remaining is the AUTHORITY figure for a desk: what its terms permit the approver to approve right now, ignoring whether the owner's pool can cover it.

Render

func Render(path string) string

Render shows the realm at "", a desk page at "desk/<id>" and a request page at "request/<id>". Labels and memos are free text and pass through the ecosystem sanitizer before hitting markdown.

RequestMemo

func RequestMemo(id int64) string

RequestMemo returns a request's raw memo (unsanitized: callers rendering it into markdown must sanitize, as this realm's Render does).

Retract

func Retract(cur realm, reqID int64)

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

Retract removes the caller's own pending request. CLAIMANT ONLY. Nothing moves.

Named Retract rather than Withdraw on purpose: every Withdraw* in this portfolio moves money, and this moves none. A claimant reading a function list should not have to open the source to learn which.

Submit

func Submit(cur realm, deskID int64, amount int64, memo string) int64

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

Submit files a reimbursement request against a desk and returns the request id. ANYONE may submit: that is the point of the realm — the party who spent the money asks, and does not need to be a signer, a delegate, or known to the desk in advance.

Nothing is reserved and nothing moves. The amount and the fee are fixed here (the fee is snapshotted from the desk, so a later AmendDesk cannot change the terms of a request already filed), and the request expires at submittedAt + RequestTTL.

Submitting against a desk whose approver is the submitter is permitted and useless: Approve refuses self-approval, so such a request can only be rejected, retracted or expired. It is not refused here because an approver may legitimately submit at a desk they do not approve, and a check at Submit would have to guess which desk the claimant meant.

Surplus

func Surplus() int64

Surplus returns H - (P + L) — out-of-band coins, sweepable.

SweepSurplus

func SweepSurplus(cur realm, denom string) int64

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

SweepSurplus sends out-of-band coins to the FeeCollector: for the pool denom, everything above tracked liabilities; for any foreign denom, the full balance. Owner pools, claimant credit and the fee pot are untouchable by construction (coinio.Sweep refuses to dip below the reserve). Anyone may call it.

With nothing out of band this ABORTS ("coinio: no surplus to sweep for <denom>") rather than returning 0. That is the primitive's choice and the right one: a sweep is a transfer, and a transfer of nothing that reports success is how a caller comes to believe a balance was moved.

TotalPooled

func TotalPooled() int64

TotalPooled returns the P term: unspent owner funds. Pending requests are NOT included; they are not liabilities.

UnclaimedCredit

func UnclaimedCredit() int64

UnclaimedCredit returns the claimants' share of the L term: approved and not yet claimed.

WithdrawFees

func WithdrawFees(cur realm) int64

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

WithdrawFees pays the entire accrued fee pot to the compile-time FeeCollector. Anyone may call it: the destination is fixed, so there is nothing for a caller to steer.

WithdrawPool

func WithdrawPool(cur realm, amount int64) int64

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

WithdrawPool returns part of the caller's UNSPENT pool and reports the pool REMAINING afterwards.

Unlike budgets, this is not a hazard to a counterparty, and the reason is the divergence in the package doc: an approved reimbursement has already left the pool and sits in the ledger as the claimant's. What an owner can still do is empty the pool under a live desk, so that the approver's next Approve fails on solvency and pending requests cannot be settled. That is the owner declining to spend their own money, which is theirs to decline; CloseDesk is the clean way to end the relationship.

WithdrawPoolAll

func WithdrawPoolAll(cur realm) int64

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

WithdrawPoolAll returns the caller's entire unspent pool and reports the amount WITHDRAWN.

The return values of the two withdraw entrypoints are deliberately asymmetric, which is worth naming rather than letting a caller discover: each returns the figure its caller does not already have. WithdrawPool was told the amount and wants to know what is left; WithdrawPoolAll knows what is left (nothing) and wants to know what it got. Returning the remainder here would hand every caller a constant 0.


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/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/spendbook, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv, strings
First-party dependenciesgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/feeledger, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/spendbook

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/reimbursements/reimbursements.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