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 state

Back to all declarations

collectionName

untyped string

Value

"Gnotchi gnomes"

collectionSymbol

untyped string

Value

"GNOME"

maxNameLen

untyped bigint

Value

(24 <untyped> bigint)

hourSeconds

int64

// rules.config.json time.hourMs / 1000

Value

3600

patCooldownHours

int64

// time.patCooldownHours

Value

12

happyWindowHours

int64

// time.happyWindowHours

Value

2

sulkyAfterHours

int64

// time.sulkyAfterHours

Value

72

meterMax

int64

// meters.max

Value

10

bellyDropEveryHours

int64

// meters.bellyDropEveryHours

Value

3

cheerDropEveryHours

int64

// meters.cheerDropEveryHours

Value

6

feedBelly

int64

// meters.feedBelly

Value

4

patCheer

int64

// meters.patCheer

Value

3

hungryAtOrBelow

int64

// meters.hungryAtOrBelow

Value

2

acornsStart

int64

// acorns.start

Value

10

plantCost

int64

// acorns.plantCost

Value

1

harvestPerPlot

int64

// acorns.harvestPerPlot

Value

3

numPlots

untyped bigint

// acorns.plots

Value

(3 <untyped> bigint)

growHours

int64

// acorns.growHours

Value

24

feedCost

int64

// acorns.feedCost

Value

1

tinkerCost

int64

// acorns.tinkerCost

Value

5

xpPat

int64

// xp.pat

Value

5

xpFeed

int64

// xp.feed

Value

5

xpHarvestPerPlot

int64

// xp.harvestPerPlot

Value

10

xpTinker

int64

// xp.tinker

Value

15

dailyCareCap

int64

// xp.dailyCareCap

Value

60

levelBase

int64

// xp.levelBase

Value

50

levelStep

int64

// xp.levelStep

Value

25

maxLevel

int64

// xp.maxLevel

Value

20

typeAtLevel

int64

// xp.typeAtLevel

Value

5

maxActionsPerCall

untyped bigint

Value

(32 <untyped> bigint)

maxBackfillHours

int64

Value

48

moodHungry

untyped string

Value

"hungry"

moodSulky

untyped string

Value

"sulky"

moodHappy

untyped string

Value

"happy"

moodContent

untyped string

Value

"content"

actionPat

untyped string

Value

"pat"

actionFeed

untyped string

Value

"feed"

actionPlant

untyped string

Value

"plant"

actionHarvest

untyped string

Value

"harvest"

actionTinker

untyped string

Value

"tinker"

validHats

map[string]bool

validHats/validCoats: unchanged from v1.

Open
OID
038150…b42d:4
validHats details

Inspect map

plotState

type

plotState: v2. One of this gnome's numPlots garden plots. growing=false means empty (plantable); growing=true means occupied since plantedAt, harvestable once growHours have passed.

Value

v2.plotState

gnome

type

gnome: v1's fields (name, hat, coat, level, gnomeType, adoptedAt) are unchanged in meaning and position; v2 adds the care-state fields SaveDay reads and writes. Per the brief ("Store meters as timestamps, not as decaying numbers") belly/cheer are stored as a last-known value plus the unix second it was set at, and meterNow below works out the current value from elapsed time whenever anything reads it -- nothing ticks on this realm's own clock, there is no scheduled job, and nothing here is updated by anything other than a SaveDay call the owning address itself signed.

Value

v2.gnome

nextID

v0.ID

Value

<zero>

soulboundExtension

type

soulboundExtension: unchanged from v1. Rule 6 ("every transfer fails") made structural. See the package doc above and v1's own long comment (realms/garden/garden.gno) for the full writeup of why this relies on the transaction boundary rather than hook ordering, and why TransferFrom being a plain method (not a crossing call) is exactly why the package-level SECURITY note applies to it.

Value

v2.soulboundExtension

Adopt

func(name string, hat string, coat string)

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.

Open
OID
038150…b42d:18
Adopt details

Inspect func

alreadyAdopted

func(addr .uverse.address) bool

alreadyAdopted: unchanged from v1. See that file's comment for why both ownerIndex and token.BalanceOf are consulted, not either alone.

Open
OID
038150…b42d:19
alreadyAdopted details

Inspect func

validNameLength

func(name string) bool

validNameLength: unchanged from v1.

Open
OID
038150…b42d:20
validNameLength details

Inspect func

validNameCharset

func(name string) bool

validNameCharset: unchanged from v1.

Open
OID
038150…b42d:21
validNameCharset details

Inspect func

hasVisibleNameContent

func(name string) bool

hasVisibleNameContent: unchanged from v1.

Open
OID
038150…b42d:22
hasVisibleNameContent details

Inspect func

invisibleLetterRunes

map[int32]bool

invisibleLetterRunes: unchanged from v1 in content -- same four codepoints -- but audit fix F3 restores v1's own \\uXXXX escape form instead of the literal invisible glyphs this file had drifted to. The content was still correct (verified by codepoint grep at audit time), but a map keyed on literal default-ignorable characters is by definition unreadable in an editor or a diff, and any formatter or copy/paste step that strips default-ignorable characters could silently empty this denylist with no failing test to catch it. \\uXXXX escapes are plain ASCII source, so that failure mode no longer exists, and TestInvisibleLetterRunesIsExactlyFourPinnedCodepoints below pins both the count and every individual codepoint so a truncated map fails loudly instead of silently. See v1's own comment for the exhaustive scan that proves this list complete against the Unicode version this toolchain ships today, and for what would make it worth re-running.

Open
OID
038150…b42d:24
invisibleLetterRunes details

Inspect map

validNameRune

func(r int32) bool

validNameRune: unchanged from v1. See that file's comment for the full rationale (allowlist not denylist, what it guarantees, what it deliberately does not -- the combining-mark exclusion recorded as an open product question in NOTES.md, and the cross-script-homoglyph residual that is accepted, not closed).

Open
OID
038150…b42d:25
validNameRune details

Inspect func

gnomeForAddr

func(addr .uverse.address) (tid string, g *v2.gnome, found bool)

gnomeForAddr: unchanged from v1.

Open
OID
038150…b42d:26
gnomeForAddr details

Inspect func

GnomeByAddress

func(addr .uverse.address) (tokenID string, name string, hat string, coat string, level int64, gnomeType string, adoptedAtUnix int64, found bool)

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.

Open
OID
038150…b42d:27
GnomeByAddress details

Inspect func

CareState

func(addr .uverse.address) (belly int64, cheer int64, acorns int64, totalXp int64, lastPatAtUnix int64, lastActionAtUnix int64, found bool)

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.

Open
OID
038150…b42d:28
CareState details

Inspect func

Render

func(path string) string

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.

Open
OID
038150…b42d:29
Render details

Inspect func

saveAction

type

saveAction is one parsed entry from a SaveDay call's action list.

Value

v2.saveAction

SaveDay

func(actionsRaw string)

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: "pat:<unixSeconds>" "feed:<unixSeconds>" "tinker:<unixSeconds>" "plant:<plotIndex>:<unixSeconds>" "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.

Open
OID
038150…b42d:30
SaveDay details

Inspect func

parseActions

func(raw string) []v2.saveAction

parseActions parses and validates the wire format SaveDay's own comment documents, and enforces maxActionsPerCall before doing any per-entry work -- a caller who sends a thousand ";"-separated entries is rejected on the delimiter COUNT alone, before strings.Split ever runs. Audit fix F5: this used to call strings.Split(raw, ";") first and check len(parts) > maxActionsPerCall second -- but Split itself allocates the full \[]string result (one allocation per entry) before that length is ever inspected, so a megabyte of semicolons allocated half a million short-lived strings before being rejected. SaveDay's own comment claims an oversized list is rejected "before a single entry is even parsed," which was true of per-entry parsing but not of this allocation. strings.Count scans the raw bytes for the delimiter without allocating a result slice, so the oversized case is now rejected without allocating anything proportional to the attacker's input at all. The caller still pays their own gas for whatever they send, so this was never a DoS on other users -- this fix makes the comment's own claim true, it does not close a cross-user vulnerability.

Open
OID
038150…b42d:31
parseActions details

Inspect func

parseUnixField

func(s string, part string) int64
Open
OID
038150…b42d:32
parseUnixField details

Inspect func

parsePlotField

func(s string, part string) int
Open
OID
038150…b42d:33
parsePlotField details

Inspect func

dayNumber

func(unixSeconds int64) int64

dayNumber is the UTC calendar day index for a unix-second timestamp -- integer division by 86400 lands exactly on 00:00 UTC boundaries, matching brief 3.1 ("a day for daily limits runs from 00:00 to 24:00 UTC") without any timezone handling, because unix time is UTC by definition.

Open
OID
038150…b42d:34
dayNumber details

Inspect func

meterNow

func(value int64, setAt int64, now int64, dropEveryHours int64) int64

meterNow ports app/js/rules.js's meterNow exactly: work out the current value from a last-known value, the unix second it was set, and how many whole dropEveryHours periods have elapsed since, clamped to \[0, meterMax].

Open
OID
038150…b42d:35
meterNow details

Inspect func

clampMeter

func(v int64) int64
Open
OID
038150…b42d:36
clampMeter details

Inspect func

canPat

func(lastPatAt int64, now int64) bool

canPat ports app/js/rules.js's canPat: lastPatAt == 0 means never patted (see gnome's own comment for why 0 is an unambiguous sentinel here, unlike in the JS version).

Open
OID
038150…b42d:37
canPat details

Inspect func

moodNow

func(belly int64, lastPatAt int64, lastHappyEventAt int64, now int64) string

moodNow ports app/js/rules.js's moodNow, in the same first-match-wins order as brief table 3.3: hungry, then sulky, then happy, then content. "Asleep" is display-only (brief 3.1) and has no on-chain representation at all, exactly as in rules.js. A gnome that has never been patted (lastPatAt == 0) reads as sulky here, immediately, even on the day it was adopted, if that day's batch never includes a pat -- ported faithfully from rules.js's own \`Math.max(g.lastPatAt || 0, ...)\`, which has the identical property (a never-patted gnome's "last attention" is treated as unix-epoch-zero, so "no pat for 72 hours" is trivially true). rules.test.js never exercises this edge case (every mood fixture there sets lastPatAt to a real recent-ish value), so this is a faithful port of the answer key's actual formula, not a verified-by-the-answer-key's-own- tests behaviour -- flagged here rather than silently relied on. See NOTES.md.

Open
OID
038150…b42d:38
moodNow details

Inspect func

xpToNext

func(level int64) int64

xpToNext/totalXpForLevel/levelFromXp port app/js/rules.js's functions of the same name, line for line in spirit: 50 + 25\*level to reach the next level, level 20 the maximum.

Open
OID
038150…b42d:39
xpToNext details

Inspect func

totalXpForLevel

func(level int64) int64
Open
OID
038150…b42d:40
totalXpForLevel details

Inspect func

levelFromXp

func(totalXp int64) int64
Open
OID
038150…b42d:41
levelFromXp details

Inspect func

assignType

func(gardenXp int64, explorerXp int64, tinkerXp int64) string

assignType ports app/js/rules.js's assignType. On a genuine tie (including the trivial three-way tie of a gnome that reached level 5 on pats and feeds alone, which count towards no type at all) this returns "" rather than guessing: the brief says "the player chooses", and this version has no chooser endpoint (StartRetrain/an explicit "choose type" call is out of scope here) -- see NOTES.md. This mirrors v1's own existing convention that an empty gnomeType is a valid, documented, permanent-until-resolved state (Render already prints "(not yet set)" for it).

Open
OID
038150…b42d:42
assignType details

Inspect func
collectionName : untyped string ="Gnotchi gnomes"
collectionSymbol : untyped string ="GNOME"
maxNameLen : untyped bigint =(24 <untyped> bigint)
hourSeconds : int64 =3600
patCooldownHours : int64 =12
happyWindowHours : int64 =2
sulkyAfterHours : int64 =72
meterMax : int64 =10
bellyDropEveryHours : int64 =3
cheerDropEveryHours : int64 =6
feedBelly : int64 =4
patCheer : int64 =3
hungryAtOrBelow : int64 =2
acornsStart : int64 =10
plantCost : int64 =1
harvestPerPlot : int64 =3
numPlots : untyped bigint =(3 <untyped> bigint)
growHours : int64 =24
feedCost : int64 =1
tinkerCost : int64 =5
xpPat : int64 =5
xpFeed : int64 =5
xpHarvestPerPlot : int64 =10
xpTinker : int64 =15
dailyCareCap : int64 =60
levelBase : int64 =50
levelStep : int64 =25
maxLevel : int64 =20
typeAtLevel : int64 =5
maxActionsPerCall : untyped bigint =(32 <untyped> bigint)
maxBackfillHours : int64 =48
moodHungry : untyped string ="hungry"
moodSulky : untyped string ="sulky"
moodHappy : untyped string ="happy"
moodContent : untyped string ="content"
actionPat : untyped string ="pat"
actionFeed : untyped string ="feed"
actionPlant : untyped string ="plant"
actionHarvest : untyped string ="harvest"
actionTinker : untyped string ="tinker"
validHats : map[string]bool Inspect
validCoats : map[string]bool Inspect
plotState : type =v2.plotState
gnome : type =v2.gnome
token : *v0.Token Inspect
ledger : *v0.PrivateLedger Inspect
nextID : v0.ID =<zero>
gnomes : *v0.Tree Inspect
ownerIndex : *v0.Tree Inspect
init.49 : func() Inspect
soulboundExtension : type =v2.soulboundExtension
Adopt : func(name string, hat string, coat string) Inspect
alreadyAdopted : func(addr .uverse.address) bool Inspect
validNameLength : func(name string) bool Inspect
validNameCharset : func(name string) bool Inspect
hasVisibleNameContent : func(name string) bool Inspect
invisibleLetterRunes : map[int32]bool Inspect
validNameRune : func(r int32) bool Inspect
gnomeForAddr : func(addr .uverse.address) (tid string, g *v2.gnome, found bool) Inspect
GnomeByAddress : func(addr .uverse.address) (tokenID string, name string, hat string, coat string, level int64, gnomeType string, adoptedAtUnix int64, found bool) Inspect
CareState : func(addr .uverse.address) (belly int64, cheer int64, acorns int64, totalXp int64, lastPatAtUnix int64, lastActionAtUnix int64, found bool) Inspect
Render : func(path string) string Inspect
saveAction : type =v2.saveAction
SaveDay : func(actionsRaw string) Inspect
parseActions : func(raw string) []v2.saveAction Inspect
parseUnixField : func(s string, part string) int64 Inspect
parsePlotField : func(s string, part string) int Inspect
dayNumber : func(unixSeconds int64) int64 Inspect
meterNow : func(value int64, setAt int64, now int64, dropEveryHours int64) int64 Inspect
clampMeter : func(v int64) int64 Inspect
canPat : func(lastPatAt int64, now int64) bool Inspect
moodNow : func(belly int64, lastPatAt int64, lastHappyEventAt int64, now int64) string Inspect
xpToNext : func(level int64) int64 Inspect
totalXpForLevel : func(level int64) int64 Inspect
levelFromXp : func(totalXp int64) int64 Inspect
assignType : func(gardenXp int64, explorerXp int64, tinkerXp int64) string Inspect