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/v4 package

Overview

Package garden is Gnotchi's pet realm, v4: garden/v3 plus a permanent face.

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, which this realm inherits whole, and realms/garden/v4/DESIGN.md, which records only what v4 changes and why. Read both before changing anything here.

v4 IS v3 PLUS A PERMANENT FACE, AND THREE SMALL REPAIRS. garden/v3 was deployed to onyx at block 336496 on 2026-10-11 and nobody adopted on it -- TotalGnomes() read 0 on 2026-10-12 -- so replacing it costs the deploy gas and nothing else: no migration, no gnome to move. What changes: every gnome carries two more PERMANENT appearance ids, eyes and nose, each a small integer validated against a compiled-in range and written exactly once, at Adopt. That is SPRITES.md's "Phase 1 -- faces" (ten eyes, eight noses: sixteen looks become 1,280), which v3's DESIGN.md section 7.3 had argued away by applying a correct argument against OPEN-ENDED appearance to a closed list nobody had asked to be open. The realm records what it is given and validates it; whether a face is CHOSEN by the player or ROLLED by the app is deliberately not decided here, because a realm cannot be edited and that decision should not be frozen in one (DESIGN.md v4 section 3). The repairs, each cheap only while nobody has adopted: the acorn softlock AUDIT-DEPLOYED.md calls G9 is made unreachable by an acorn reserve in SaveDay; a reserved buyer typed in upper case no longer creates a listing nobody can buy; and the test-only fee override is gone from production code. Everything else -- the SetRule ratchet, the garden-settles-to-seller rule, the IsUserCall gates on Adopt and Buy, the sale, the wire format, every read -- is v3's, unchanged, with v3's reasoning kept in place below.

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.

Function

Adopt

func Adopt(cur realm, name, hat, coat string, eyes, nose int64)
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.

NEW IN v4: eyes AND nose. Two integers, eyes in [0, numEyes) and nose in [0, numNoses), checked after the hat and coat so a caller with several mistakes hears them in the order the arguments are written. An id outside its range aborts the whole call and mints nothing -- an unknown face is refused, never defaulted, because a default would silently give every mistyped adoption the same face and a face is permanent. THE REALM DOES NOT CARE HOW THE NUMBERS WERE ARRIVED AT. A client may offer the player a choice, or roll on a weighted table and submit the result, or anything in between; this function records what it is given and validates it, and nothing on chain can tell the two apart. That neutrality is deliberate and argued in realms/garden/v4/DESIGN.md section 3: a realm cannot be edited, so the choice-versus- rarity decision must not be frozen in one. Its honest consequence is stated there too -- rarity on this realm is a property of client discipline, not of the contract, and a raw gnokey caller can always ask for the rarest face. The signature change breaks every existing Adopt caller (the app's live.js builds [name, hat, coat]); v4 DESIGN.md section 4 lists them.

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/v4" -func "Adopt" -args $'' -args $'' -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/v4" -func "Adopt" -args $'' -args $'' -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