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

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.
  • The acorn softlock (new in v4, AUDIT-DEPLOYED.md G9): a batch that spends acorns may not end with nothing planted and fewer than acornReserve left. Checked once, after the apply pass, against the batch's END state, so "feed then plant" in one call is fine and the refusal names the whole batch rather than one action. See acornReserve.

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