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.

v2 source realm

Package garden is Gnotchi's Phase 1 on-chain slice, v2: everything v1's garden realm does (Adopt, GnomeByAddress, Ren...

Overview

Package garden is Gnotchi's Phase 1 on-chain slice, v2: everything v1's garden realm does (Adopt, GnomeByAddress, Render, soulbound, one gnome per address, the validated name/hat/coat rules, the OriginSend refusal -- every fix from v1's three audit rounds, carried over rather than rewritten from memory, see below for exactly which lines are unchanged from realms/garden/garden.gno) plus the new thing Phase 1 actually needs: SaveDay, the "one signature saves the whole day" call the brief's section 5.2 and the build brief's own paragraph 2 promise and that the deployed v1 realm does not have.

v1 (realms/garden/garden.gno) is deployed to the onyx testnet at gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v1 and, per DECISIONS.md, cannot be edited -- this file is deliberately a sibling, not a modification. v1 is left completely untouched as the deployed record.

THIS FILE IS NOT DEPLOYED. Authored and tested locally only, per this assignment's own brief: no gnokey, no key material, no deploy.

Everything below this comment that is unchanged from v1 (the soulboundExtension, Adopt's validation chain, the name-charset allowlist/denylist, Render's path validation, GnomeByAddress) is carried over with its original comments intact, because those comments are the audit trail for *why* each check exists -- deleting them to make this file shorter would silently discard that trail. New material (the care-state fields on gnome, SaveDay and its helpers, CareState) is clearly marked "v2:" in its own comments.

SECURITY: the same warning v1 carries applies here unchanged and is reprinted in full, because it is load-bearing and not something a reader should have to go find in a different file to understand this one: no code in this realm, now or later, may ever recover() around a call into ledger or token (grc721's PrivateLedger/Token). The soulbound 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 soulboundExtension 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 one several call-frames away -- would silently convert every refused transfer into a permanent, successful one. Don't add 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 now also applies to SaveDay's own validate-then-commit pattern below: SaveDay never recovers from its own internal panics either, by design -- see that function's comment.

Functions 5

func Adopt

crossing Action
1func Adopt(cur realm, name, hat, coat string)
source

Adopt: unchanged from v1 in every validation it performs (same order, same messages, same OriginSend refusal, same caller derivation). v2 only adds initialising the new care-state fields at the bottom, where v1 only set level/gnomeType/adoptedAt.

func CareState

Action
1func CareState(addr address) (belly, cheer, acorns, totalXp, lastPatAtUnix, lastActionAtUnix int64, found bool)
source

CareState is v2's new read: the SaveDay-relevant state GnomeByAddress deliberately doesn't carry. belly/cheer are returned already decayed to time.Now() (per the brief, "work out meter values from stored timestamps when they're read" applies to every reader, not just SaveDay's own internal bookkeeping) -- a caller never sees a stale stored value and has to re-derive the decay itself.

func GnomeByAddress

Action
1func GnomeByAddress(addr address) (tokenID, name, hat, coat string, level int64, gnomeType string, adoptedAtUnix int64, found bool)
source

GnomeByAddress: unchanged from v1 -- same signature, same fields, same meaning. v2's new care-state fields are deliberately NOT added to this tuple, so that nothing about what v1 already promised here changes; see CareState below for the new read.

func Render

1func Render(path string) string
source

Render: unchanged from v1 except one new bullet (Acorns) -- see this function's own history in v1 for the full writeup of why path is bech32-validated before anything derived from it is ever echoed back into the page. v2 deliberately does not also render belly/cheer/garden-plot state here: CareState above is the read for that, and growing gnoweb's single markdown page to show every care-state field is a product decision for a later pass, not an engineering requirement this assignment's brief asks for.

func SaveDay

crossing Action
1func SaveDay(cur realm, actionsRaw string)
source

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

ENCODING, and why it looks like this rather than like the brief's own "SaveDay(gnomeID, actions)": a public realm function reachable by `maketx call` or Adena's DoContract can only take arguments gno.land's own call-argument decoder knows how to build -- read directly from this toolchain's gno.land/pkg/sdk/vm/convert.go (convertArgToGno): primitive scalars (bool, string, every sized int/uint/float) and fixed-size-or-slice []byte (base64). There is no case there for a slice of structs, and no amount of Gno-side type cleverness changes what the transaction's wire format can carry. So "actions as a list" has to arrive as a single string this function parses itself, not as a []Action the chain's own tooling has no way to construct from a gnokey/Adena call. actionsRaw is 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 means an empty action list (a legal no-op call). See NOTES.md for this decision in full.

Also absent from the brief's own signature: gnomeID. v1 and v2 both enforce one gnome per address with no transfer and no delegation of any kind -- there is no second gnome an address could ever mean by "gnomeID", and no path by which SaveDay could legitimately be asked to act on any gnome but the caller's own. Dropping the parameter removes a whole class of "whose gnome is this call actually touching" bug before it can exist, rather than adding a check that would only ever have one correct answer anyway. See NOTES.md.

THREAT MODEL -- this assignment's own brief asks for this explicitly: actionsRaw is client-supplied and this realm is the only authority over it, full stop. Specifically defended below, each against a concrete hostile input:

  • Replay (resubmitting an already-applied batch, in whole or in part): every action's timestamp must be strictly greater than lastActionAt, the gnome's own high-water mark of the latest action any past SaveDay call has ever actually applied. A replayed batch's first action is, by definition, <= that mark.
  • Duplicate actions within one call: the same strict-increase rule applies action to action within the list (actions[i].at < actions[i+1].at required), so two actions sharing one timestamp -- the only way a client could claim two things happened at once -- is rejected regardless of which two kinds they are.
  • Future-dated actions: every timestamp must be <= time.Now().Unix(), this call's own block time. Nothing claimed to have happened hasn't happened yet.
  • Past-dated actions, at or before adoption: subsumed by the replay check above -- lastActionAt starts at adoptedAt (see Adopt), so no action can ever be dated before adoption either.
  • Unlimited backfill after a long dormancy (audit fix F2, an owner decision -- see NOTES.md): the two checks above bound an action relative to lastActionAt (below) and to now (above), but say nothing about how far *below* now an action may be, so nothing stopped a player dormant for a year from backfilling a year of daily care in one sitting. maxBackfillHours (48) closes this with two parts, both needed: (1) any action older than the window is rejected outright, with a message a player can understand, and (2) the floor the ordering/replay check above enforces is itself clamped forward to at least now-maxBackfillHours before that loop runs, rather than left at the gnome's raw, possibly year-old lastActionAt -- so even if (1) were ever weakened or removed by a future edit, the floor the replay check already enforces would independently keep the backfillable span bounded. See the validation loop below for both parts and NOTES.md for the owner's reasoning on the 48-hour number.
  • Inflated action counts (e.g. fifty pats claimed in one day): canPat's 12-hour spacing makes more than two pats a day arithmetically impossible inside one call regardless of what the list claims, and feed/plant/tinker are each gated by this gnome's own acorn balance, which this exact set of rules is the only thing that can ever increase -- there is no action, in this call or any other exported function, that credits acorns without the realm itself having applied the matching cost somewhere upstream. The realm's own economy is the rate limit; no separate per-type daily counter is needed on top of it.
  • A thousand actions in one list: rejected by maxActionsPerCall before a single entry is even parsed -- parseActions counts the ";" delimiters first and panics on an oversized count before calling strings.Split at all, so an oversized actionsRaw never pays the cost of allocating a huge split result just to be rejected a moment later.
  • XP inflation via the daily cap: rawCareXp for the whole call is mood-adjusted, then clamped to whatever remains of dailyCareCap for the UTC calendar day this call's own actions fall on -- tracked by the gnome's own dailyCareXp/dailyCareDay accumulator, not by anything scoped to one call. Audit fix F1: earlier code capped only the current call's own XP at dailyCareCap and had no record of what a PREVIOUS call the same day had already spent, so a second same-day call got its own independent 60-XP allowance (two calls, 60 each, 120 against a cap of 60). See the cap/mood block below and the gnome struct's own comment on dailyCareXp/dailyCareDay.

ATOMICITY: every check below runs against a local copy (work) of the caller's own gnome, taken by value at the top of this function. Nothing is written back to the gnomes tree until every action in the list has validated and applied cleanly against that copy; any panic before the final commit leaves the persisted gnome completely untouched, with no partial effect to undo. This is deliberately a plain Go/Gno value copy, not a recover()-and-rollback: see the package-level SECURITY note for why a recover() anywhere in this realm is never the right tool, including here.

Dependencies

Source Files 2