v5 source realm
Package garden is Gnotchi's pet realm, v5: the free, allowlisted, drand-rolled, hold-many realm MINT.md designs end t...
Package garden is Gnotchi's pet realm, v5: the free, allowlisted, drand-rolled, hold-many realm MINT.md designs end to end. It inherits garden/v4's machinery -- the SaveDay shape, meters, garden plots, levels, and the List/Buy/Cancel trading surface -- but NOT v4's invariants (MINT.md section 6). garden/v4 is live, immutable and untouched by this file; its two gnomes stay exactly where they are forever (MINT.md section 6, BATCH-ONE.md section 0).
WHAT CHANGES FROM v4, IN ONE TABLE, EACH CITED TO THE DOCUMENT THAT DECIDED IT:
- Adopt(name, hat, coat, eyes, nose) is GONE. Nothing is chosen by a caller any more. DECISIONS.md 2026-10-15 deprecates the DIY picker as a product direction; MINT.md section 1a/section 2 replace it with a roll.
- An ALLOWLIST gates who may ever claim (MINT.md section 11: "gate issuance, never the secondary market"). Admin-managed, additive and removable, never touching the mint itself once a claim has spent its roll.
- ONE ROLL PER WALLET, FOREVER, enforced independently of whether the resulting gnome is still held (MINT.md section 1a/section 2.3). hasRolled is permanent; nothing in this file clears it, and selling the gnome does not refund the roll.
- FREE. Claim refuses coins exactly as v4's Adopt refuses them (MINT.md section 1a/2.5, GAMES.md rule 6: "random when earned, exact when bought... free: random is allowed").
- THE ROLL IS TWO-PHASE, SETTLED BY DRAND (MINT.md section 10, the load-bearing finding of this whole file). A single-transaction roll is peekable two ways, verified against gno.land's own source and binary rather than assumed from Ethereum's model: vm/qeval can call a crossing function for free with no signature, and `gnokey maketx call -simulate only` previews a real signed call against the last committed block with no gas and no broadcast. Neither tool is the cause; the cause is that gno.land exposes NO on-chain entropy at all -- chain/runtime has ChainHeight and nothing else, math/rand is PCG seeded (0,0) on every node forever, and there is no shipped VRF -- so any single-transaction formula, however it is dressed up, is something a caller can evaluate offline for every block it might land in before ever deciding to broadcast. The fix MINT.md section 10.5 settles on, and this file builds, is a commit/reveal split across this project's own deployed drand verifier: Claim spends the one-time roll and pins a ROUND THAT DOES NOT EXIST YET; Settle, permissionless, computes the result once that round is actually published. One player signature, not two -- the player signs Claim only.
- PARTS FROM A WEIGHTED TABLE (ECONOMY.md section 3), not a player's choice, with three named one-of-ones (Hob, Thistle, Wick -- BATCH-ONE.md section 7) capped at one each.
- COUNTED RARITY. Every part's population is tracked on chain and readable by anyone -- MINT.md section 13's "nine gnomes in the world have this nose" has to be a chain fact, and EyesCount/NoseCount/HatCount/CoatCount below are that fact, not an app claim layered on top.
- MINT ONE, HOLD MANY, PLAY ONE (MINT.md section 2a). Buy's one-gnome-per-address refusal is GONE -- deliberately, and only from Buy, never from Claim (see Claim's hasRolled gate, which is the cap that still matters). An address may hold any number of gnomes; exactly one is ever AWAKE, tracked by activeToken, and only the awake one can ever gain a cared day, a streak, a level or XP. SaveDay and ChooseType resolve through activeToken, never through "the" gnome for an address, because there no longer is one.
- SWITCHING (Wake) is free, at most once per UTC day (MINT.md section 2a), the same day-boundary discipline SaveDay's own dailyCareDay already uses.
- THE SECONDARY MARKET IS OPEN: Buy/List/Cancel carry over v4's shape and fee (MINT.md section 11), with no allowlist on buying and no hold cap.
- THE LOCK: the three named one-of-ones cannot be listed until they have lockCaredDays cared days; every ordinary gnome trades from day one (MINT.md section 12/13, "Option A" -- lock only the named one-of-ones, recommended there and NOT YET CONFIRMED BY THE OWNER, which is why lockCaredDays is one named constant and nothing else in this file assumes its value). A gnome's own caredDayCount never decreases and is not reset by Buy, so once a named gnome unlocks it stays unlocked through every future sale.
- A GENESIS GNOME, minted to this realm's own address at deploy, non-transferable, never listable, separate from the 24-gnome batch (DECISIONS.md 2026-10-15: "the genesis gnome belongs to nobody, permanently, on purpose"). Its name is not yet settled anywhere in this project's documents; genesisGnomeName below is a placeholder, named as one.
WHAT IS DELIBERATELY OUT OF SCOPE FOR THIS FILE, EACH A SCOPING DECISION MADE BY THIS PASS, NOT A GAP IN THE BRIEF THAT ASKED FOR IT. Recorded here in the open the same way MINT.md records its own open questions, so a future reader finds the reasoning in one hop:
- NO SetRule/FreezeRules TUNABLE ADMIN SURFACE. v4's ruleBand/SetRule/FreezeRules machinery is real and proven, but every care-mechanic number below is a fixed constant at v4's own deploy defaults instead. The brief this file was written against lists eleven concrete requirements and none of them is "tunable rules"; adding v4's whole admin-ratchet surface on top of everything already new here would have roughly doubled this file's size and its audit surface for a feature nobody asked for in this pass. A v6 (or a v5.1 companion design note) can add it back following v4's own pattern exactly if the owner wants it.
- NO PLAYER-CHOSEN NAME. v4's name/validNameRune/validNameCharset/hasVisibleNameContent machinery (three audit rounds' worth, per v4's own comment) existed to validate a string nobody in this brief asked this realm to accept any more -- nothing is chosen by a caller at mint time, including a name. A gnome is identified by its tokenID and, for the three one-of-ones, by Character(tokenID); the app is free to let an owner caption their own gnome off chain. Dropping this also removes an entire validation subsystem's audit surface for no loss against the brief.
- NO OpenBatch ADMIN SURFACE. MINT.md section 6 floats an owner-callable OpenBatch for adding future batches' rosters; the task this file was built against names a single 24-gnome batch (BATCH-ONE.md) and does not ask for a mechanism to open a second one. The batch here is sized once, at deploy, by the batchCap/numNamedEditions constants below. A garden/v6 is the natural place for OpenBatch if a second batch is ever wanted, the same way every other irreversible-shape decision in this project has gotten its own version rather than a retrofit.
A REAL FLAW FOUND AND FIXED WHILE DESIGNING Settle, RECORDED HERE RATHER THAN QUIETLY FIXED AND FORGOTTEN -- the same discipline MINT.md section 10 models for its own single-transaction finding. An earlier version of this design picked the three named one-of-ones by drawing "without replacement" from a shrinking pool: remainingTotal = batchCap - settledCount, remainingNamed = 3 - namedAssignedCount, and a claimant's pick = seed % remainingTotal. THAT IS PEEKABLE AND GAMEABLE, even though the seed itself is honestly drand-derived and unforgeable: because remainingTotal only ever shrinks as OTHER claimants settle, and Settle is permissionless and callable at any later time, a claimant whose seed is already fixed (the moment their pinned round publishes) can simply precompute, for every possible remainingTotal from 1 to batchCap, whether that value would win them a named edition -- and then wait, costing nothing, until the rest of the batch happens to settle into a state that does. The fix below removes every dependency on shrinking, observable, time-varying state from the decision: a claimant's own named-edition pick is `seed % batchCap`, against a modulus that is a COMPILE-TIME CONSTANT, not a moving count, so there is nothing left to wait for -- the only residual order-dependence is which of possibly several colliding claimants (bad luck, not strategy: nobody can change their own pick by choosing when to settle) gets a named edition that more than one of them rolled, and that is resolved by whichever Settle call lands on chain first.
A SECOND AUDIT PASS NAMED THIS PRECISELY: "settling promptly", said above and in an earlier version of Settle's own comment, OVERSTATES what the residual actually rewards. It is not "whoever claimed first" or "whoever cared most" -- pendingClaim.claimedAt is never consulted by the tie-break, and Settle is permissionless, so a bystander (a searcher, a bribed relayer, a claimant's own bot) can win a collision for whichever side pays for transaction-ordering priority, regardless of who rolled or claimed first. Considered and rejected: tie-breaking on claimedAt instead. It sounds like the honest fix, but it is not reachable without a worse trade-off -- Settle finalizes irrevocably (it mints a real token to a real owner, who may already have acted on it), so honoring an earlier claimedAt would mean either refusing to finalize ANY collision candidate until every possible earlier claimant's round has published and settled (reintroducing exactly the wait-and-see window section 10 and this fix exist to close, now around a deadline instead of a pool size) or reaching back to UNDO an already- settled mint (which nothing in this realm, or any grc721-based realm in this project, has ever claimed to be able to do, and which would be a strictly worse property than the one being fixed). What actually holds, stated precisely rather than aspirationally: nobody can improve THEIR OWN odds by choosing when to settle (the property section 10 is about, and the property this fix actually guarantees); the residual is an ordinary transaction-ordering race over a LOW-PROBABILITY, LOW-STAKES event (three slots in a batch of 24, decided among whichever handful of claimants happen to collide), the same shape of race every permissionless action on every chain is already subject to, not a new hole this design opened. See Settle's own comment for the arithmetic.
SETTLE CAN NOW PAY FOR ITS OWN BEACON RELAY, OWNER-APPROVED 2026-10-15, THE DAY AFTER THIS FILE WAS FIRST WRITTEN. The two-phase roll above depends on beacon/v2 actually having the pinned round's drand signature by the time Settle runs, and until this change that was somebody else's problem: a relay has to happen, by a human or a bot, running outside this realm and outside this project's control, or a claimant who has already spent their one-and-only roll is stuck mid-mint through no fault of their own -- found live, not hypothetically, while building this file: the project's own beacon was observed ~2 days stale. The fix the owner asked for ("can we have the mint transaction pay the beacon though") is `SettleWithSignature` below: the player's own app fetches the pinned round's signature from drand's public API (the same `https://api.drand.sh/<chain>/public/<round>` this file's own test fixtures are fetched from) and passes it straight through; Settle relays it itself, in the same transaction, before computing the result. One transaction, and the player pays its gas -- TO THE NETWORK, same as any other transaction's gas, NOT to this project, so it never touches the free-mint promise (MINT.md section 14: "gas is still a payment, just not to us... the operator takes no stake"). Three things this buys, in the owner's own order of importance:
- IT COSTS THE PROJECT NOTHING. The alternative to a player-paid relay is an operator-run one -- a cron job or a bot calling beacon.Publish on a schedule, which is real recurring GNOT spend with no revenue to offset it (MINT.md section 14 forecloses an unlock fee, batch one is free end to end). This change deletes that cost rather than budgeting for it.
- A PLAYER CAN NEVER BE BLOCKED BY THIS PROJECT'S OWN HOUSEKEEPING. The stuck-mid-mint state above is now unreachable for anyone willing to fetch their own round's signature: their own Settle call makes the round available, with no dependency on this project, a third-party relay, or anyone else's goodwill.
- THE BEACON GETS FRESHER AS A SIDE EFFECT, FOR EVERYONE ELSE USING IT. beacon/v2 is a shared, permissionless, on-demand record (see its own package doc) -- a round this realm's own claimants cause to be published is published for every other consumer of that beacon too, not just for this realm.
THREE THINGS WORKED OUT BY QUERYING THE LIVE CHAIN, NOT ASSUMED, because this is new cross-realm write surface and this project's own history (the SPLASH beacon migration, MEMORY.md) is built from exactly the kind of interrealm detail that gets guessed wrong:
- WHAT beacon.Publish DOES ON AN ALREADY-PUBLISHED ROUND. Fetched byte-for-byte from the live deployed realm itself (gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2) via the public onyx-1 RPC's `vm/qfile` query, 2026-10-15, and diffed identical against this project's own vendored copy at realms/_vendor/beacon/v2/beacon.gno -- it PANICS, it does not no-op: `func Publish(cur realm, round int64, sigHex string) { if Published(round) { panic ("beacon: round already published") } ... }`. SettleWithSignature therefore checks `beacon.Published` itself BEFORE ever calling Publish, exactly once, and never calls Publish at all if the round is already there -- calling it unconditionally would make this function fail every time a relay (another claimant's own SettleWithSignature, or anyone else) beat it to the punch, which given Settle's own permissionless, callable-by-anyone design is an ordinary and expected race, not an edge case.
- THAT Publish IS REACHABLE CROSS-REALM AS `beacon.Publish(cross(cur), round, sigHex)`, and that it carries no caller-identity check at all for this function to trip over. Confirmed two ways: the live source pulled in point 1 shows Publish's body never reads `cur` or calls `cur.Previous()` -- it is permissionless by whoever relays, not merely by whichever address -- and this exact call shape is already proven working, not merely plausible, by this file's OWN test harness: garden_test.gno's `publishAs` helper has called `beacon.Publish(cross(cur), round, sigHex)` from this package since the day this file was written, and every test that calls it passes. SettleWithSignature's own call is the identical shape with `cur` being Settle's own realm parameter instead of a test helper's. (`cur.Previous()` inside beacon.Publish would resolve to THIS realm, garden/v5, when called this way rather than to the human wallet that called Settle -- a real interrealm fact worth recording even though Publish does not currently branch on it: if beacon/v2 ever grows a caller-identity check, re-read this paragraph before assuming Settle's relay still works.)
- GAS. Measured on this toolchain with r/splash/beacon/zz_gas_test.gno's own method (`gno test -run TestZZGas -print-runtime-metrics=true -v`, net of this package's own fixed per-test-call overhead): a bare Settle today costs ~6.2M gas; beacon.Publish alone costs ~18.5M gas on THIS realm's measurement (the same order of magnitude as beacon/v2's own documented 16,970,375, re-measured rather than quoted because toolchain and realm storage both shift it). SettleWithSignature's worst case -- the round not yet published at all -- is therefore on the order of ~25M gas total, call it 1% of the 3e9 block gas limit and a few hundredths of a GNOT at this chain's gas price. Reasonable: it is roughly four times a bare Settle, not an order of magnitude more, it still leaves room for well over a hundred such settlements in a single block, and it replaces a cost this project would otherwise have paid itself, recurringly, forever, to keep the beacon fresh by some other means.
THE SHAPE: TWO ENTRY POINTS, ONE SHARED CORE, NOT ONE FUNCTION WITH AN OPTIONAL SIGNATURE. `Settle(cur realm, claimant address)` is UNCHANGED in behaviour and UNCHANGED in signature -- every existing test and every existing caller that already knows (or does not care) whether the round is published keeps working exactly as before, at exactly its old gas cost, with no signature to fetch or supply. `SettleWithSignature(cur realm, claimant address, sigHex string)` is new, and is for a caller that HAS a signature and wants its transaction to publish the round if nobody else already has. A Gno realm's exported functions take a fixed argument list -- there is no overloading, and an optional/variadic last argument would still force every caller, including every test and the app's own simpler "it's already published" path, to pass an empty string they have no use for, for a case (publishing) that is the EXCEPTION, not the rule, since most settlements land after some earlier claimant or relay has already published the shared round. Two named entry points make the exception visible in the function name instead of buried in an argument, and cost nothing extra: both share one unexported `settleCore`, so there is exactly one copy of the draw-and-mint logic, not two drifting in parallel. And it is SAFE for either one to be called in either actual on-chain state: Settle still refuses cleanly, with its original message, if the round genuinely is not published yet; SettleWithSignature still succeeds, with its supplied signature simply going unused, if the round turns out to already be published by the time it runs (point 1's race) -- neither caller has to know in advance which state they are in, they only have to know whether they personally HAVE a signature to offer.
45
func ActiveGnome
ActionActiveGnome returns the tokenID that is awake for addr right now, or found=false if nothing is (never claimed or bought a gnome, or sold the only one and has not Woken another since).
func AdminAllow
crossing ActionAdminAllow adds addr to the allowlist, so it may call Claim. Adding an address a second time is a harmless no-op. Adding an address that has already rolled changes nothing -- hasRolled is checked independently and is permanent regardless of allowlist state.
func AdminDisallow
crossing ActionAdminDisallow removes addr from the allowlist. It does NOT, and cannot, undo a roll already spent: hasRolled is a separate, permanent record, and this function never touches it. Removing an address that was never allowed, or that has already claimed, is a harmless no-op.
func BalanceOf
Actionfunc BatchStatus
ActionBatchStatus: mintedCount is reservations made by Claim (gates batchCap); settledCount is completions made by Settle (always <= mintedCount, the gap being pending, unsettled claims); namedMinted is how many of the three named one-of-ones have actually been minted so far -- read live off characterOwner rather than a separate counter, so there is only ever one source of truth for "has this edition been taken".
func Buy
crossing ActionBuy purchases the listed gnome tokenID. v4's validate-then-commit shape, unchanged, MINUS the one-gnome-per-address refusal: MINT.md section 2a removes the hold cap from Buy specifically, and only from Buy -- Claim's hasRolled gate is the cap that still matters, and this function never touches it.
func Cancel
crossing ActionCancel withdraws the caller's own listing on tokenID. Only the seller who created it may.
func CareState
Actionfunc CaredDays
Actionfunc CharacterOwner
ActionCharacterOwner resolves one of the three named one-of-ones, or the genesis gnome under its own name "genesis", to its tokenID, once minted. (Second-audit-pass nitpick: an earlier version of this comment said only "one of the three named one-of-ones", which stopped being true the moment init() registered "genesis" here too -- see init()'s own comment for why that registration is safe and never touches the three editions' cap.)
func ChooseType
crossing Actionfunc Claim
crossing ActionClaim spends the caller's one-and-only roll on this realm and pins a drand round that does not exist yet. It mints NOTHING -- Settle does that, once the pinned round is actually published -- because the whole point of this split is that the result must not be computable, by anyone, including this transaction's own caller, at the moment the roll becomes irrevocable (MINT.md section 10.2: gno.land has no on-chain entropy at all, so anything decidable inside one transaction is decidable by the caller before broadcasting it).
WALLET-ONLY, the same reasoning as v4's Adopt: a gnome minted to a realm's address could never be recovered, so a batching, sponsor or multicall realm must never be able to spend a roll on anyone's behalf. The `gnokey maketx run` shape is refused by the identical gate, for the identical reason (IsUserCall is false for both).
func CoatCount
Actionfunc CollectionInfo
Actionfunc EyesCount
Actionfunc FeeRules
Actionfunc GardenState
Actionfunc GenesisTokenID
Actionfunc GnomeByToken
Action1func GnomeByToken(tokenID string) (hat, coat string, eyes, nose int64, character string, level int64, gnomeType string, adoptedAtUnix int64, found bool)GnomeByToken: the basic facts for one token -- hat, coat, eyes, nose, character (empty for an ordinary gnome), level, calling and the moment it was minted.
func HasRolled
Actionfunc HatCount
Actionfunc IsAllowlisted
Actionfunc IsLocked
ActionIsLocked reports whether tokenID is currently refused by List's lockCaredDays check -- true only for a named one-of-one with fewer than lockCaredDays cared days; false for every ordinary gnome, always, and false for a named one-of-one once it has enough cared days.
func LastWakeDay
Actionfunc List
crossing ActionList offers tokenID, which the caller must own, for sale. The lock: a named one-of-one (character != "") cannot be listed until it has lockCaredDays cared days; an ordinary gnome has no such restriction, trading from the moment it is minted (MINT.md section 12/13).
func Listing
Actionfunc ListingsPage
Actionfunc MeterState
Actionfunc Mood
Actionfunc NoseCount
Actionfunc OwnerTokens
ActionOwnerTokens returns every tokenID addr currently holds, in no particular order. Unbounded and unpaginated deliberately: batchCap is 24 and the genesis gnome is permanently unreachable to any wallet, so no address can ever hold more than batchCap tokens, which is a small, fixed upper bound this call is always safely within.
func PendingClaim
Actionfunc Progress
Actionfunc Provenance
Actionfunc QuoteFee
Actionfunc Render
func SaveDay
crossing Actionfunc Settle
crossing ActionSettle computes and permanently mints the result of claimant's pending roll, once the round Claim pinned has actually been published to the beacon by SOMEBODY -- another claimant's own SettleWithSignature, a relay, anyone. It does not publish anything itself; call SettleWithSignature instead if the round is not published yet and the caller has its signature to offer. ANYONE may call this -- the claimant themself, the project's own relayer, a stranger -- because by the time it can succeed at all, nothing is left to decide: the drand signature for the pinned round either exists publicly or it does not, and the parts this mints are a pure function of (that signature, the claimant's own address), fixed the instant the round is published, not the instant Settle happens to be called. Simulating this call before calling it for real previews a fact, not a choice -- exactly the property MINT.md section 10.5 names as the whole fix: "reading the result afterward is a free query, same as any other realm read."
Refuses coins for the same reason every other non-paying function here does: there is no banker to return them, and this is a free, permissionless computation, never a payment.
func SettleWithSignature
crossing ActionSettleWithSignature is Settle, plus: if claimant's pinned round has not been published yet, this function publishes it ITSELF first, in this same transaction, using sigHex -- the round's drand signature, which the caller's own app fetches read-only from drand's public API (https://api.drand.sh/<chain>/public/<round> or any mirror) and passes straight through unmodified. This is the function a player's app should call once it has fetched a signature; Settle (above) remains for a caller with no signature to offer, or that already knows (or does not care) that somebody else has published the round.
SAFE EITHER WAY THE ROUND TURNS OUT TO BE, which is the one property this function exists to guarantee and the live chain fact its shape is built around (see the package doc): beacon/v2's own Publish PANICS, not no-ops, when the round is already published, so this function checks beacon.Published first and only calls Publish if that check says no. If the round was published by someone else (another claimant's own SettleWithSignature, a relay, anyone) between the moment the caller fetched sigHex and the moment this transaction lands, sigHex is simply never used -- this function still succeeds, exactly as Settle would have. A bad sigHex, by contrast, is a real failure: beacon.Publish rejects it with its own "beacon: invalid signature" panic, which aborts this whole call (and the claim stays pending, to be retried with a correct signature or left for Settle once someone else relays it).
Refuses coins for the same reason Settle does and every non-paying function here does: there is no banker to return them, and this is a free, permissionless computation -- the only payment anywhere in this call is the ordinary GNOT gas the caller's own transaction spends, to the network, never to this project (MINT.md section 14).
func TokensPage
ActionTokensPage: the global token enumerator, hollowmere/v1's own pattern (TotalClaimants/ ClaimantsPage) applied here, because v4's own missing-enumerator lesson (DECISIONS.md:780-791, arena/v1 shipping with no challenge inbox and no way to ever add one) is unavailable to fix after deploy and twenty lines now is cheaper than "unavailable forever" later.
func TotalGnomes
Actionfunc TotalListings
Actionfunc TotalSupply
Actionfunc Wake
crossing ActionWake makes tokenID the caller's awake gnome. Free, and limited to once per UTC day -- the same day-boundary discipline SaveDay's own dailyCareDay already uses, chosen over a GNOT fee for the reason MINT.md section 2a gives: "a cost that cannot be bought, only lived".