MustNew
Ask AI
MustNew is New but panics on error.
Command
gnokey query vm/qeval -remote "https://rpc.gno.land" -data "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/spendbook.MustNew()"
Result
Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.
Package spendbook is a pure allowance primitive for realms that need to let one party spend another party's funds, up to a bounded amount, over many transactions. It is to *delegated spending* what duebook is to *time*: it holds no coins, performs no effects, imports no chain APIs, and owns no package-level state. The importing realm owns a *Book, supplies the clock, and performs its own effects.
The ecosystem's existing allowance primitive, gno.land/p/jaekwon/allowancesender/v0, is scoped to a single call and says so in its own documentation:
1"Allowance survives across N txs but not N+1 txs." Use a session
2counter pattern in the granter realm; this package's Close is
3binary, not deadline-based.
It then declines block-height expiry on the grounds that it "would require runtime cooperation". That is true only for a package that reads the clock itself. spendbook does not: every operation takes `now` as an argument, which makes the window arithmetic a pure function and the whole package unit-testable without a chain. Three application realms in the searched sources each hand-rolled this mechanism internally rather than importing one; see DISCOVERY.md.
An allowance is not a balance. It is a *permission with a ceiling that refills*. The owner says "this spender may draw at most PerPeriod per window, at most Total ever, until ExpiresAt, and I can revoke it now":
1if err := book.Spend(id, spenderKey, now, amount); err != nil {
2 panic(err) // revoked, expired, over the window cap, over the total
3}
4// ... the realm moves the coins here, under its own authority
Spend checks every bound and CONSUMES the allowance in the same call, before returning. The realm then acts. Because the realm performs the effect itself, no closure, callback, or capability ever crosses a realm boundary. spendbook cannot be handed code to run, so it cannot be tricked into running the wrong code.
A window is derived from a fixed origin — the Anchor set at Open — by integer division:
1index(now) = (now - Anchor) / PeriodLength
so the boundaries are the same for everyone and a spender cannot reset a window by choosing when to call. A sliding "PerPeriod per trailing N blocks" window would let a spender draw the full cap at the end of one trailing span and again at the start of the next; anchoring removes the choice. The refill is a RESET, not a carry: an unused allowance does not accumulate into the next window, because the point of the cap is to bound worst-case loss per window, and a carry would unbound it.
PeriodLength == 0 means one window that never rolls: a pure lifetime allowance, where PerPeriod is the only cap that matters.
An owner who could raise PerPeriod mid-window would be able to undo the bound a spender is relying on, and — worse in the other direction — an owner who drains a relationship and then lowers terms retroactively could strand a spender mid-settlement. The rule here is one-sided and enforced in Amend:
This is the same adversarial problem that r/samcrew/agent_registry_v2 solves by locking its price while any credit entry exists; the shape here is a deferral rather than a lock, so terms can still move.
With PeriodLength == 0 there is no next boundary, so a loosening amendment is refused with ErrNoBoundary rather than silently queued forever. PeriodLength itself is IMMUTABLE: changing it would move every future boundary, which is an enlargement vector wearing a different hat.
Revoked allowances are removed, not archived. IDs are allocated monotonically and never reused, so a revoked ID can never be resurrected and there is nothing to keep. Live allowances are capped per Book at construction, so storage is bounded by maxLive regardless of how many allowances have ever existed. Audit history belongs in the consuming realm's events.
An EXPIRED allowance is not self-removing, and bounded storage is not the same as a usable Book: Spend refuses it forever while it still holds a slot. Reap exists for exactly that and is permissionless, because an expired allowance can authorize nothing and so removing it takes nothing from anyone.
What this package does NOT bound is how many of a Book's slots ONE owner may hold — maxLive is a single global figure, so any owner permitted to Open can exhaust it and lock out the rest. That is a policy question with no answer a primitive can pick (per-owner quota? first-come? owner whitelist?), so it is left where the authentication already lives: the consuming realm decides who may Open, and a Book shared across mutually untrusting owners needs its own per-owner cap on top of this one. Named here rather than implied, because "bounded" reads like "safe to share".
SUPPLY A REAL CLOCK. spendbook cannot verify that `now` came from runtime.ChainHeight() or time.Now().Unix(). A realm that lets a caller choose `now` has handed the spender the power to roll the window at will, which defeats every cap in this package. Pass the chain's own clock, and use the same units for the life of the Book.
AUTHENTICATE THE SPENDER KEY. spendbook compares the key string it is given against the one recorded at Open. It cannot tell whether that string is the real caller. Derive it from cur.Previous().Address(), never from an argument the caller supplies.
SPEND BEFORE SENDING. Spend consumes the allowance and returns; the realm then moves coins. Doing it in the other order means a failed send leaves the allowance consumed, or a successful send leaves it unconsumed — both are real losses. State first, effect second.
AN ALLOWANCE IS NOT CUSTODY. spendbook authorizing 100 does not mean 100 exists. The realm must check its own held balance separately; this package bounds permission, not solvency.
MustNew is New but panics on error.
gnokey query vm/qeval -remote "https://rpc.gno.land" -data "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/spendbook.MustNew()"
New returns an empty Book. maxLive caps simultaneously live allowances and must be in [1, MaxLiveLimit].
gnokey query vm/qeval -remote "https://rpc.gno.land" -data "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/spendbook.New()"