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.

garden/v3 package

Overview

Package garden is Gnotchi's pet realm, v3.

WHY THIS EXISTS, in one paragraph, because a realm cannot be edited and the reason has to travel with the code. garden/v2 accepts `plant:0:<ts>` and `harvest:0:<ts>`, enforces the grow timer, moves the acorns -- and exposes no way to read a plot back. Its whole garden is write-only on chain. The app therefore hides the garden from any player who connects a wallet (app/js/home.js gates the garden card on `!gnome.chainProof`), and since harvesting is the only acorn income, a connected player's acorns can only ever go down. The live site now apologises for this in six places (app/js/config.js's WALLET_TRADEOFF_SENTENCE). There is no fix that is not a new realm. GardenState below is that fix; everything else here is either the same correction applied to eight more unreadable fields, or deliberately unchanged from v2.

Full design, including every v2 limitation with line numbers, the migration argument, and the list of things deliberately left out: realms/garden/v3/DESIGN.md. Read that before changing anything here.

GNOMES ARE TRADEABLE HERE, AND THE SALE COUNTER IS THE ONLY DOOR. TRADING-RULES.md records the owner's ruling of 2026-10-10 -- "gnomes AND items are tradeable, and a cut on every trade of both is a must" -- and MARKETPLACE-V1.md records the structural fact that decides the shape: a royalty implemented as a transfer hook sees transfers, not payments, so the only way to make the cut unavoidable is for the contract holding the asset to refuse every transfer path except its own sale function. That is what this realm does. There is no Approve, no TransferFrom, no gift, no burn, no withdrawal; a registered extension panics on every ledger movement unless Buy below is moving exactly that token right now. See the SALE section and DESIGN.md section 9. The fee and its recipient are the three SALE CONSTANTS at the very top of this file, decided by the owner on 2026-10-11 for the onyx testnet; init still gates them and refuses a value that could not work.

DEPLOY STATUS IS DELIBERATELY NOT RECORDED IN THIS FILE, and neither is a claim about whether anyone has decided to deploy it. This comment ships on chain exactly as written and cannot be edited afterwards, so any status asserted here is frozen at authoring time and becomes a lie the moment it changes. garden/v2/gnomod.toml said "NOT deployed" in just such a line, was deployed hours later, and now carries that false claim on chain permanently. Live status lives in the repo root's STATUS.md and DECISIONS.md, which can still be corrected; the authority above both is the chain itself (`vm/qfile` on this path).

============================================================================ THE ONE RULE THAT REPLACES v2's MISTAKE ============================================================================

v2's bug class is not "a getter was forgotten". It is that a field was added to persisted state without a reader, on a contract that can never grow one. So:

Example
1EVERY PERSISTED FIELD HAS AN EXPORTED READER, AT DEPLOY TIME. THERE IS NO SECOND CHANCE.

Anyone adding a field to `gnome` below must add it to a reader in the same change. TestEveryStoredFieldIsReadable pins the current field list against the current readers, value by value, so the failure is at least loud and local. Gno has no reflection to assert over, so that test cannot *detect* a new field -- the rule is still a discipline and this comment is not pretending otherwise.

The corollary, which is why this file has no "extensible" blob getter: EXTENSIBILITY IS A FICTION ON AN IMMUTABLE CONTRACT. A versioned self-describing string cannot carry a field that did not exist at deploy time any more than a positional tuple can. This surface is designed for completeness now, not for growth later, because there is no later.

============================================================================ SECURITY: NO recover(), EVER. Carried over from v1/v2/hollowmere/arena verbatim in intent. ============================================================================

No code in this realm, now or later, may recover() around a call into ledger or token (grc721's PrivateLedger/Token). The sale-gate guarantee below is real, but it is NOT that the ledger write never happens -- it does, before the panic -- it is that an *uncaught* panic aborts the whole enclosing transaction, so that write (and everything else the transaction did) is discarded and never committed on chain. p/nt/grc721/v0/types.gno says this outright about extension hooks like saleGateExtension below: "Hooks run after the ledger write so they observe the post-movement state; only a caller that recovers from the panic keeps that write." A recover() anywhere between a ledger call and the transaction boundary -- even one added for an unrelated reason, even several call-frames away -- would silently convert every refused transfer into a permanent, successful one.

This is not limited to code textually inside this file: a pure `p/` package this realm calls runs in garden's own call frame too, so a helper as innocuous as `Try(func(){ ledger.TransferFrom(...) })` imported from some future `p/` dependency would break the same guarantee just as surely as a recover() written here directly.

The same rule applies to SaveDay's, ChooseType's and Buy's validate-then-commit pattern below: none recovers from its own internal panics, by design. Every check runs against a local value copy and nothing is written back until the whole call has validated; a panic before the commit leaves persisted state completely untouched, with no partial effect to undo. Buy extends the rule to money: the ledger write, the index writes and the two coin sends all happen after the last check that can fail, so a sale either happens whole or not at all -- there is no reachable state in which the gnome moved and the coins did not, or the reverse.

Functions

Acorns

func Acorns(addr address) (acorns int64, found bool)
Ask AI Claude ChatGPT

Acorns is the player's own acorn balance, read directly rather than only through CareState, because acorns are a PLAYER's property here and not a gnome's (ECONOMY.md:65-67) and the read should say so. An address that has never held a purse gets 0, false -- never a bare 0 that a caller could mistake for a real empty purse. A player who has SOLD their gnome still has a purse, and this says so: it is keyed on the purse, not on the gnome, because the purse outlives the gnome and is what a later Adopt or Buy will find waiting.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Acorns()"

Result

Adopt

func Adopt(cur realm, name, hat, coat string)
Ask AI Claude ChatGPT

Adopt mints one gnome to the caller. Every validation v1 and v2 performed is carried over in the same order with the same messages -- the name charset allowlist, the invisible-letter denylist, the one-gnome-per-address rule, the OriginSend refusal -- because those checks are the output of three audit rounds and re-deriving them from memory is how they get lost.

TWO THINGS CHANGE BECAUSE GNOMES CAN NOW BE SOLD. First, an address that has sold its gnome has no gnome, so it may adopt again -- "one gnome per address" is a statement about the present, not a lifetime count, and adoption is free for a fresh address anyway, so refusing it to a seller would punish only the honest. Second, the starting purse is granted ONCE PER ADDRESS, EVER: only if this address has never held acorns. A seller keeps their purse (acorns belong to the player, ECONOMY.md:65-67), so a re-adopting seller must not be topped back up to acornsStart, and a buyer who already has a purse from an earlier gnome keeps that one. Without this, sell-and-readopt would refill acorns for free.

THE CUSTODY QUESTION IS CLOSED: ONLY A WALLET MAY ADOPT. realms/garden/NOTES.md recorded it in full and reserved it for the owner: cur.Previous().Address() is not restricted to a human wallet, so a realm that called Adopt would become the permanent, unrecoverable owner of a gnome, with no burn, admin or transfer to undo it. v1 and v2 left that open. On 2026-10-11 the owner chose the accident case over the sponsor case -- an orphaned gnome permanently owned by some batching or multicall realm is a real person's lost gnome, and it can never be undone -- so Adopt now refuses any caller that is not a direct wallet call, with the same IsUserCall gate Buy already uses (DESIGN.md section 7.5).

What the gate is, exactly, read from this toolchain's chain/runtime: IsUserCall() is "pkgPath == """, true for a `gnokey maketx call` (or a wallet's MsgCall) straight from a user and false for every code realm, INCLUDING the ephemeral realm a `gnokey maketx run` script executes in (that one is IsUserRun, not IsUserCall). So a sponsor realm, a batching realm and a run script are all refused, and none of them can ever be let back in on this realm: a v4 with an explicit "adopt for" path is how that door would reopen. The refusal comes first, before anything is read or written, so a refused caller gets this reason and not a later one.

A consequence worth stating: since Buy is also wallet-only, NO REALM CAN EVER OWN A GNOME here, by construction. The audit's G-2 worry -- a realm owning and listing a gnome so that Buy pays a price to an address with no banker, where it is burned -- has no remaining path.

Params

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "Adopt" -args $'' -args $'' -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "Adopt" -args $'' -args $'' -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

BalanceOf

func BalanceOf(addr address) int64
Ask AI Claude ChatGPT

BalanceOf is 0 or 1 for every address, forever: one gnome per address, no burn, and Buy refuses a buyer who already holds one. Exposed anyway rather than replaced by a bool, because a GRC721 consumer expects this shape.

An invalid address owns nothing, so 0 is the CORRECT answer here and not an error signal -- this function deliberately cannot distinguish "invalid address" from "owns none", because there is nothing to distinguish. Acorns(addr) below is the reader that does draw a distinction (absent versus zero), and it draws it about something where the difference is real. An earlier version of this comment claimed the opposite of the line underneath it; corrected here, before it could ship and say that permanently.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.BalanceOf()"

Result

Buy

func Buy(cur realm, tokenID string)
Ask AI Claude ChatGPT

Buy purchases the listed gnome tokenID, paying its seller and the fee recipient in the same call. The caller must be a wallet, must not own a gnome, must be the reserved buyer if there is one, and must attach EXACTLY the listing's price + fee in ugnot and nothing else.

VALIDATE, THEN COMMIT -- AND THE COMMIT PHASE CANNOT FAIL. Every refusal below happens before the first write. After the "commit" line nothing remains that can panic on chain: the ledger move is to an owner we have just verified with a gate we have just opened; the two coin sends are for amounts we have just checked equal the envelope (so the BankerTypeOriginSend cap cannot refuse them), from this realm's own address (which the envelope has credited before this code runs), to addresses validated at List time and at init. Payment is therefore atomic with transfer by construction, and not only because an uncaught panic would abort the transaction -- though it would, and that is the second lock.

NO REENTRANCY IS POSSIBLE AND THIS IS WHY, STATED SO IT CAN BE CHECKED RATHER THAN TRUSTED: the only calls Buy makes outside this file are grc721's TransferFrom (a /p/ package running in this realm's own frame, whose only hook is saleGateExtension above) and banker.SendCoins (a native that credits a bank balance and runs no code at the recipient). Neither can call back into this realm. State is nonetheless fully written before the sends, so even if a future chain gave coin recipients a hook, re-entering Buy for this token would find no listing and a different owner.

EXACTLY, NOT AT LEAST. Accepting more than the total would make this realm a bank for the difference, with no withdrawal path; accepting a different denomination would strand it. The abort returns the whole envelope to the buyer, so a wrong amount costs gas and nothing else. Listing() publishes the exact total a wallet must attach.

Param

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "Buy" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "Buy" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

Cancel

func Cancel(cur realm)
Ask AI Claude ChatGPT

Cancel withdraws the caller's own listing. Only the seller can; nobody else -- not the admin, not a reserved buyer -- can cancel or alter anyone's listing.

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "Cancel" -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "Cancel" -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

CareState

func CareState(addr address) (belly, cheer, acorns, totalXp, lastPatAtUnix, lastHappyEventAtUnix, lastActionAtUnix int64, found bool)
Ask AI Claude ChatGPT

CareState is v2's CareState plus the two things it left out: lastHappyEventAt (the mood input v2 stored at its own line 197 and exposed nowhere, so the app substituted lastActionAt, which is a different number -- a harvest-only day advances lastActionAt and not lastHappyEventAt), and acorns now read from the player's balance rather than the gnome.

belly and cheer are returned already decayed to time.Now(), as v2's were. MeterState below returns the raw stored pair for a caller that wants to run app/js/rules.js's own meterNow against this realm's state and check the two agree.

The signature is NOT positionally compatible with v2's CareState, unlike GnomeByAddress above. Nothing cross-realm reads CareState -- only the app does -- and inserting lastHappyEventAt where it belongs was worth more than preserving a tuple shape for a single caller that has to change anyway.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.CareState()"

Result

CaredDays

func CaredDays(addr address) (daysCsv string, found bool)
Ask AI Claude ChatGPT

CaredDays returns the UTC day numbers this address saved care on, newest first, comma separated -- e.g. "20371,20370,20368". At most caredWindowDays of them; empty if never.

THE STREAK RULE IS DELIBERATELY NOT HERE. app/js/rules.js's streakFrom is the single tested implementation of it and takes exactly this list (as "YYYY-MM-DD", one line of JS from a day number -- rules.js:233 already does that conversion). Porting its ISO-week rest-day attribution onto an immutable contract would mean two implementations of one rule, and getting it subtly wrong would make the chain and the browser disagree about the streak forever. The chain stores facts; the browser owns the rule. DESIGN.md section 2.3.

The encoding (a uint64 mask relative to lastCaredDay) is internal and no caller needs to know it; TestCaredMaskRoundTrips pins it.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.CaredDays()"

Result

ChooseType

func ChooseType(cur realm, kind string)
Ask AI Claude ChatGPT

ChooseType sets this gnome's calling, once, permanently, at level typeAtLevel or above.

THIS REPLACES v2's DERIVED TYPE, AND IT IS THE WHOLE FIX FOR "ADVERTISES SOMETHING UNREACHABLE". v2 called assignType(xpGarden, 0, xpTinker) -- a literal 0 for explorer, because it had no Visit call -- so Explorer could never be assigned to any gnome, ever; and because the app hides the garden and the tinker bench from a connected player, such a player reached level 5 with 0 and 0, tied three ways, and got "" instead. Four independent sources already said the answer is a chooser rather than a tally:

  • app/js/rules.js's own assignType returns {choose: tied} on a tie, documented "Ties: player chooses".
  • garden/v2/garden.gno's own comment: "the brief says 'the player chooses', and this version has no chooser endpoint".
  • LOOP.md:99-101: "at level 5 the player CHOOSES the calling rather than having it derived from tallies that can only ever say Gardener."
  • DECISIONS.md:643-646: the tallies are unbalanced by construction, because gardening funds the gnome's food and tinkering does not, so the derivation was never a choice.

WHAT IT COSTS, stated rather than glossed: the type stops being evidence of what a player did and becomes evidence of what they picked. arena/v1's type edge then reflects a choice rather than a history -- strictly better than reflecting a history that could only ever have one answer, but a different claim, and anyone reading that edge should know it changed.

One choice, no retrain. rules.config.json has a retrainDays key; this realm implements nothing for it (DESIGN.md section 7.4).

Param

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "ChooseType" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "ChooseType" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

CollectionInfo

func CollectionInfo() (name, symbol string)
Ask AI Claude ChatGPT

CollectionInfo / TotalSupply / BalanceOf / OwnerOfToken: the GRC721 read surface.

THESE EXIST BECAUSE AN AUDIT FOUND THE ONE PLACE THIS REALM'S OWN RULE DID NOT HOLD. Every field of the gnome struct has a reader; `token` and `ledger` did not. They are persisted realm state (the token is created inside garden's own storage, so there is no separate grc721 realm an indexer could query instead), and before these four functions the only read of any of it was token.TotalSupply() interpolated into Render's markdown, where no caller can consume it as a typed value.

What that cost, permanently, if it had shipped: no wallet could list the gnome, no indexer could enumerate holders, no other realm could verify ownership except through garden's own GnomeByAddress, and -- the sharpest one -- there was NO tokenID-to-owner reader at all, because ownerIndex is owner-to-tokenID only. A tokenID observed in an Adopt event could not be resolved back to an owner without brute-forcing GnomesPage. garden/v2 has the identical gap and can never close it.

nextID is the one piece of persisted state with no direct reader, and that is deliberate rather than missed: it is a seqid counter equal to the number of adoptions (there is no burn, and a sale moves a token without minting one), so TotalSupply() below is the same number by construction. Recorded so the next reader does not have to work that out.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.CollectionInfo()"

Result

FeeRules

func FeeRules() string
Ask AI Claude ChatGPT

FeeRules publishes the fee exactly as this realm will charge it, for the same reason Rules() publishes the game numbers: the chain's own value is the one that applies, and a client that can read it has no reason to hold a copy. Everything here is frozen for the realm's life.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.FeeRules()"

Result

FreezeRules

func FreezeRules(cur realm)
Ask AI Claude ChatGPT

FreezeRules ends tuning permanently. One admin-only, irreversible call; afterwards SetRule panics forever and this realm is exactly as frozen as garden/v2.

This is what makes the whole tunable-config decision safe to defer rather than get right before deploy: the owner can deploy tunable, run Check A, apply LOOP.md's numbers and then freeze -- or freeze on day one and never tune. Both outcomes are reachable from this realm. Only one is reachable from v2.

There is no unfreeze, on purpose. A reversible freeze is not a freeze.

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "FreezeRules" -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "FreezeRules" -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

GardenState

func GardenState(addr address) (plantedAtCsv, readyAtCsv string, readyCount int64, found bool)
Ask AI Claude ChatGPT

GardenState is the read garden/v2 does not have, and the reason this realm exists.

plantedAtCsv and readyAtCsv are each numPlots comma-separated unix seconds in plot order, with 0 for an empty plot -- e.g. "1791600000,0,0" / "1791686400,0,0". Two parallel fixed-width lists rather than one encoded blob, because the app's qeval reader already splits plain strings and because a reader that has to parse a schema is a reader that can parse it wrongly.

readyCount is how many plots are harvestable AT THIS MOMENT, computed by the realm against each plot's own stored readyAt. The caller could derive it, but only by also knowing the chain's current block time, which is exactly the thing a browser gets wrong (see SaveDay's comment on DECISIONS.md:500-515). So the chain answers it.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.GardenState()"

Result

GnomeByAddress

func GnomeByAddress(addr address) (tokenID, name, hat, coat string, level int64, gnomeType string, adoptedAtUnix int64, found bool)
Ask AI Claude ChatGPT

GnomeByAddress has the BYTE-IDENTICAL signature to garden/v2's, on purpose: anything already written against v2 -- arena/v1's own GnomeByAddress call, app/js/adventure- villagers.js, app/js/live.js's qeval tuple parser, which reads these eight values positionally -- repoints at this realm with a one-line import change and no rewrite.

That matters because arena/v1 is deployed and imports garden/v2 immutably, so a gnome adopted here cannot use arena/v1 at all and battle needs an arena/v2. Keeping this signature stable is the one thing this realm can do to make that a one-line change rather than a port. See DESIGN.md section 6.3.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.GnomeByAddress()"

Result

GnomesPage

func GnomesPage(offset, limit int) []string
Ask AI Claude ChatGPT

GnomesPage returns up to limit owner addresses in ownerIndex's own stable key order, starting at offset. limit is CLAMPED to maxGnomesPageSize rather than rejected, so a caller asking for too much still gets a gas-bounded page instead of an aborted call; a negative offset or a non-positive limit are caller mistakes with no sane page to return.

Paging with offset 0, then offset+len(page), until a page shorter than its requested limit comes back, covers every owner exactly once with no gaps and no duplicates for any snapshot where the tree does not grow between calls. TestGnomesPagePagesEveryOwnerExactlyOnce proves that directly rather than assuming it from avl.Tree's doc comment.

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.GnomesPage(,)"

Result

List

func List(cur realm, priceUgnot int64, buyer address, days int64)
Ask AI Claude ChatGPT

List offers the caller's own gnome for sale.

priceUgnot may be 0: that is the gift shape, and the buyer still pays the floor. buyer is the zero address ("") for anyone, or one named address other than the seller. days is 1..maxListingDays. Listing again replaces the standing listing; it is how a price is changed, and because Buy demands the exact total of the listing it finds, a buyer who prepared coins for the old price is refused rather than charged the new one.

Listing locks nothing. The seller keeps caring for the gnome; care only ever adds to what the buyer gets, and a listed gnome that goes hungry is the seller's own problem to price.

Params

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "List" -args $'' -args $'' -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "List" -args $'' -args $'' -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

Listing

func Listing(tokenID string) (seller address, priceUgnot int64, buyer address, listedAtUnix, expiresAtUnix, feeUgnot, totalUgnot int64, found bool)
Ask AI Claude ChatGPT

Listing is one gnome's standing offer, including the EXACT total a wallet must attach to Buy. found is false when the gnome is not listed. An expired listing is still returned, with its expiresAt, so a client can tell "expired" from "never listed"; Buy refuses it.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Listing()"

Result

ListingsPage

func ListingsPage(offset, limit int) []string
Ask AI Claude ChatGPT

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.ListingsPage(,)"

Result

MeterState

func MeterState(addr address) (belly, bellyAtUnix, cheer, cheerAtUnix int64, found bool)
Ask AI Claude ChatGPT

MeterState returns the RAW stored meter pairs, undecayed. v2 exposed only the decayed values, which is lossy in a way that matters here: a caller cannot check the chain's decay against app/js/rules.js's meterNow if it is only ever handed the chain's answer. With this, the browser can run its own formula over the chain's own inputs -- which is how a repeat of the mood divergence in DESIGN.md section 1.5 gets caught in a day instead of in a year.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.MeterState()"

Result

Mood

func Mood(addr address) (mood string, found bool)
Ask AI Claude ChatGPT

Mood returns the realm's OWN mood for this gnome, right now.

This exists because the chain's mood is what applies the XP modifier, and because v2 proves a divergence here is not hypothetical: v2 ported rules.js's mood faithfully, rules.js was then fixed so a never-patted gnome is "new" rather than "neglected", and the deployed realm and the shipped browser have returned different moods for the same gnome ever since, with no way to notice from outside. v3 matches the current rules.js (see moodNow) and publishes its answer so the next divergence is observable.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Mood()"

Result

Progress

func Progress(addr address) (xpGarden, xpTinker, harvestCount, lastTinkerAtUnix, caredDayCount, dailyCareXpSpent, dailyCareDay int64, found bool)
Ask AI Claude ChatGPT

Progress is the rest of the stored state: the lifetime counters, the tinker clock and this UTC day's care-XP spend. Every one of these is a field garden/v2 writes and cannot read back.

dailyCareXpSpent is the one a client most needs and v2 most lacked: the chain caps care XP per UTC DAY and app/js/rules.js caps it per CALL, so a browser that does not subtract this will promise XP the chain then refuses. dailyCareDay says which UTC day that spend belongs to, so a client can tell "60 spent today" from "60 spent on a day that has since rolled over".

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Progress()"

Result

Provenance

func Provenance(addr address) (adoptedAtUnix, acquiredAtUnix, saleCount int64, found bool)
Ask AI Claude ChatGPT

Provenance is the reader for the two fields a sale adds to the gnome.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Provenance()"

Result

QuoteFee

func QuoteFee(priceUgnot int64) (feeUgnot, totalUgnot int64)
Ask AI Claude ChatGPT

QuoteFee is what a sale at priceUgnot costs the buyer, before anyone lists anything.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.QuoteFee()"

Result

Render

func Render(path string) string
Ask AI Claude ChatGPT

Render is the gnoweb page. Unlike v2's, it shows the garden -- which is the whole point of this realm, and a player should be able to see their plots without a client at all.

path is bech32-validated before anything derived from it is echoed back into the page, the same guard v1, v2, hollowmere and arena all use, for the same reason: path is whatever a URL put after the realm path, so it is attacker-controlled, and gnoweb renders this return value as markdown. A rejected path is never reflected at all, which is strictly safer than escaping it.

Param

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Render()"

Result

RuleBands

func RuleBands() string
Ask AI Claude ChatGPT

RuleBands publishes the tunable surface itself: "key=min..max" for every band, in the same order Rules() uses.

Without this, "the admin can only tune inside bands" is a claim in a document. With it, anyone can read the actual limits off the chain and check what this realm will and will not accept, without trusting DESIGN.md, this comment, or the owner.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.RuleBands()"

Result

Rules

func Rules() string
Ask AI Claude ChatGPT

Rules publishes every number this realm actually uses, as "key=value;key=value;...", fixed values first in the order of DESIGN.md's agreement table, then the tunables in ruleBands order.

THIS IS THE PIECE v2 HAD NO ANSWER TO. v2 kept thirty-odd values hand-copied from app/js/rules.config.json with no reader, so no drift check between the chain and the browser was possible even in principle -- nothing on chain could be asked what growHours was. With this, anyone can: gnoweb, curl, or a node test that compares each key against rules.config.json and fails on any mismatch.

AND IT FIXES THE DIRECTION OF CORRECTION, which is the part that matters on an immutable contract. v2's two copies could drift and neither was authoritative. Here the chain is authoritative and, for a tunable, is the only place a value can change -- so a client should READ this and use it, not hold a second copy and hope.

WHAT IS NOT DONE, stated rather than implied: the node-side half of that check does not exist. The follow-up is one test comparing this output against rules.config.json, plus making app/js/rules-bridge.js prefer these values for a connected player. Neither is written. Until they are, this is a capability and not a check.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.Rules()"

Result

SaveDay

func SaveDay(cur realm, actionsRaw string)
Ask AI Claude ChatGPT

SaveDay is the brief's "one daily save": the day's pats, feeds, plants, harvests and tinker task submitted as a single list in one signed call.

THE WIRE FORMAT IS UNCHANGED FROM v2, deliberately. It is proven on chain (TESTNET.md:51-60, heights 310579/310601/310681) and app/js/live.js already builds it, so changing it would cost app work and buy nothing. Semicolon-separated entries, each colon-separated:

Example
1"pat:<unixSeconds>"
2"feed:<unixSeconds>"
3"tinker:<unixSeconds>"
4"plant:<plotIndex>:<unixSeconds>"
5"harvest:<plotIndex>:<unixSeconds>"

e.g. "pat:1791600000;feed:1791601000;plant:0:1791602000". An empty string is a legal no-op. A list, rather than a []Action parameter, because gno.land's own call-argument decoder (gno.land/pkg/sdk/vm/convert.go, convertArgToGno) can only build primitive scalars and []byte from a transaction -- there is no case there for a slice of structs, and no Gno-side type cleverness changes what the wire format can carry. No gnomeID parameter either: one gnome per address, no delegation, so there is no second gnome a caller could mean and no path by which this could legitimately act on anyone else's. A sale changes which address that is, and nothing else here: the seller's next SaveDay finds no gnome, and the buyer's must date every action after the sale (Buy resets lastActionAt).

TIMESTAMPS ARE BLOCK TIME, NOT WALL CLOCK, AND A CLIENT THAT FORGETS THAT FAILS EVERY TIME. DECISIONS.md:500-515: the first on-chain SaveDay failed with "action timestamp cannot be in the future" because the client sent `date -u +%s` while block time ran ~3 seconds behind wall clock, measured not assumed, and "a browser sending Date.now() will therefore fail essentially always". The fix is client-side -- read the chain's own latest block time and subtract a small margin. This realm deliberately does NOT add a future tolerance: DECISIONS' own warning is that a tolerance near midnight would let a caller claim the next UTC day's XP allowance early.

THREAT MODEL. actionsRaw is client-supplied and this realm is the only authority over it. Carried over from v2 whole, including both of its audit fixes:

  • Replay, in whole or in part: every timestamp must be strictly greater than lastActionAt, the gnome's own high-water mark. A replayed batch's first action is by definition <= that mark.
  • Duplicate actions inside one call: the same strict-increase rule applies action to action, so two actions sharing a timestamp are rejected whatever kinds they are.
  • Future-dated actions: every timestamp must be <= this call's own block time.
  • Actions at or before adoption: subsumed by the replay check, since lastActionAt starts at adoptedAt.
  • Unlimited backfill after a long dormancy (v2's audit fix F2, an owner ruling): maxBackfillHours closes this twice over. Any action older than the window is rejected outright with a message a player can understand, AND the floor the replay check enforces is itself clamped forward to now-maxBackfillHours before the loop runs, so the bound does not rest on a single comparison a future edit could weaken.
  • Inflated action counts: pat spacing bounds pats per day arithmetically regardless of what the list claims (two a day at the default 12-hour cooldown, four if the owner ratchets it to the band floor of 6 -- v2's version of this note said "more than two a day is impossible", which was true of v2's fixed constant and is not true here), and feed/plant/tinker are each gated by the caller's own acorn balance, which this exact set of rules is the only thing that can increase. The economy is the rate limit. Even the floor case is far under dailyCareCap, so nothing here is a way around the cap.
  • A thousand actions in one list: rejected on the delimiter COUNT, before strings.Split allocates anything proportional to the input (v2's audit fix F5).
  • XP inflation via the daily cap (v2's audit fix F1): the whole call's raw care XP is mood-adjusted, then clamped to whatever remains of dailyCareCap FOR THE UTC CALENDAR DAY, tracked by the gnome's own accumulator rather than anything scoped to one call.

ATOMICITY: every check runs against local copies -- `work` for the gnome, `acorns` for the balance -- taken at the top. Nothing is written back until the whole list has validated and applied cleanly, so any panic before the final commit leaves persisted state completely untouched. A plain value copy, not a recover()-and-rollback; see the package SECURITY note.

Param

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "SaveDay" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "SaveDay" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

SetRule

func SetRule(cur realm, key string, value int64)
Ask AI Claude ChatGPT

SetRule changes one tunable number, inside its band, if the rules are not frozen.

Only adminAddress. No admin transfer. Nothing here can touch a gnome, a balance, an XP total or a level -- the only state this function can reach is ruleValues, and only for a key that is in ruleBands, and only to a value inside that key's band. See ruleBands' own comment for why this exists at all and why the bands make it defensible; DESIGN.md section 5 for the full argument, including the explicit note that this is the one decision in v3 the owner should actively approve or veto before deploy, and that vetoing it is a deletion rather than a redesign.

Params

Command

# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.

gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "SetRule" -args $'' -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3" -func "SetRule" -args $'' -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx
  

TotalGnomes

func TotalGnomes() int64
Ask AI Claude ChatGPT

TotalGnomes / GnomesPage: the enumerator garden/v2 has no equivalent of.

This project has already paid for a missing enumerator once. DECISIONS.md:780-791 records that arena/v1 "exposes no way to list challenges -- confirmed with vm/qfuncs against the deployed realm, not assumed from the source -- and a realm cannot be edited", so there is no challenge inbox and there cannot be one; "if an inbox is ever wanted, it is an arena/v2 plus a migration". hollowmere/v1 learned that and shipped TotalClaimants/ClaimantsPage. garden/v2 did not, so no leaderboard, no village roster and no "who is playing" query is possible against it at all. Twenty lines, unavailable forever if omitted.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.TotalGnomes()"

Result

TotalListings

func TotalListings() int64
Ask AI Claude ChatGPT

TotalListings / ListingsPage: the board. Same shape and same reasoning as TotalGnomes / GnomesPage -- this project has paid for a missing enumerator once already. The page is tokenIDs, oldest key first; resolve each with Listing() and OwnerOfToken(). Expired listings are included until their seller lists again or cancels.

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.TotalListings()"

Result

TotalSupply

func TotalSupply() int64
Ask AI Claude ChatGPT

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v3.TotalSupply()"

Result