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.

v4 state

Back to all declarations

saleFeeRecipient

untyped string

// gnotchi-opponent, owner's choice

Value

"g1ylpe9juk8cafgf9rt09wzuz85ug2lyfk5metyv"

saleFeeRateBps

int64

// 5% of the declared price

Value

500

saleFeeFloorUgnot

int64

// 1 GNOT flat minimum per sale

Value

1000000

collectionName

untyped string

Value

"Gnotchi gnomes"

collectionSymbol

untyped string

Value

"GNOME"

adminAddress

untyped string

Value

"g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar"

maxNameLen

untyped bigint

Value

(24 <untyped> bigint)

numEyes

int64

Value

10

numNoses

int64

Value

8

acornReserve

int64

Value

2

hourSeconds

int64

Value

3600

daySeconds

int64

Value

86400

numPlots

untyped bigint

// acorns.plots -- see DESIGN.md 4.4 for why this can never be tunable

Value

(3 <untyped> bigint)

plantCost

int64

// acorns.plantCost

Value

1

feedCost

int64

// acorns.feedCost

Value

1

acornsStart

int64

// acorns.start

Value

10

meterMax

int64

// meters.max

Value

10

patCheer

int64

// meters.patCheer

Value

3

hungryAtOrBelow

int64

// meters.hungryAtOrBelow

Value

2

happyBonusPct

int64

// xp.happyBonus \* 100

Value

10

happyWindowHours

int64

// time.happyWindowHours

Value

2

sulkyAfterHours

int64

// time.sulkyAfterHours

Value

72

maxLevel

int64

// xp.maxLevel

Value

20

typeAtLevel

int64

// xp.typeAtLevel -- when ChooseType unlocks

Value

5

xpPat

int64

// xp.pat

Value

5

xpFeed

int64

// xp.feed

Value

5

xpHarvestPerPlot

int64

// xp.harvestPerPlot

Value

10

xpTinkerTask

int64

// xp.tinker

Value

15

maxActionsPerCall

untyped bigint

Value

(32 <untyped> bigint)

maxBackfillHours

int64

Value

48

caredWindowDays

uint

Value

64

maxGnomesPageSize

untyped bigint

Value

(100 <untyped> bigint)

ugnotDenom

untyped string

Value

"ugnot"

maxPriceUgnot

int64

Value

1000000000000000

maxFeeRateBps

int64

Value

10000

maxListingDays

int64

Value

30

maxListingsPageSize

untyped bigint

Value

(100 <untyped> bigint)

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"

typeGardener

untyped string

Value

"gardener"

typeExplorer

untyped string

Value

"explorer"

typeTinkerer

untyped string

Value

"tinkerer"

keyGrowHours

untyped string

Value

"growHours"

keyHarvestPerPlot

untyped string

Value

"harvestPerPlot"

keyFeedBelly

untyped string

Value

"feedBelly"

keyBellyDrop

untyped string

Value

"bellyDropEveryHours"

keyCheerDrop

untyped string

Value

"cheerDropEveryHours"

keyPatCooldown

untyped string

Value

"patCooldownHours"

keyTinkerCost

untyped string

Value

"tinkerCost"

keyDailyCareCap

untyped string

Value

"dailyCareCap"

keyLevelBase

untyped string

Value

"levelBase"

keyLevelStep

untyped string

Value

"levelStep"

keySulkyPenaltyPct

untyped string

Value

"sulkyPenaltyPct"

kindMin

untyped string

// lower is kinder, so the default sits at max and may only fall

Value

"min"

kindMax

untyped string

// higher is kinder, so the default sits at min and may only rise

Value

"max"

validHats

map[string]bool

validHats/validCoats: unchanged from v1/v2. v3's DESIGN.md section 7.3 records why appearance is not the "open-ended list of parts" ECONOMY.md asks for: on an immutable realm an open-ended list is either validated against a compiled-in allowlist (exactly as closed as this) or it accepts arbitrary strings, which would undo validNameRune's three audit rounds one field over, since Render echoes into markdown. That argument is correct and v4 keeps it. What v3 got wrong was the conclusion drawn from it -- "so keep hat and coat" -- because a face chosen from ten eyes and eight noses is not open-ended; it is two more fields of exactly this kind, and v4 adds them as integer ranges (numEyes/numNoses above) rather than as a third and fourth map.

Open
OID
02488d…6d24:4
validHats details

Inspect map

validTypes

map[string]bool

validTypes: the three callings ChooseType accepts. All three are reachable in this realm, which is the point -- v2's assignType was called with explorer XP hard-coded to 0 (garden/v2/garden.gno:823), so Explorer was permanently unreachable there, and for a connected player who never saw a garden or a tinker bench none of the three was. See DESIGN.md section 3.4.

Open
OID
02488d…6d24:8
validTypes details

Inspect map

ruleBand

type

ruleBand is one tunable number: its key, its deploy default, and the closed interval SetRule will accept. kindEnd records which end of that interval is kinder to the player, and is what TestEveryBandIsPlayerSafe checks the default against.

Value

v4.ruleBand

ruleBands

[]v4.ruleBand (len=11)

ruleBands is the whole tunable surface of this realm. Nothing outside this table can be changed after deployment, by anyone, ever. WHY THIS TABLE EXISTS AT ALL, since no other realm in this project has one. DECISIONS.md records that the build brief gave garden and arena a Pause(), that an AI instruction overrode it, that consequently "no realm has an admin, a pause or a settable parameter, and every number is a frozen constant", and that "whether the next realms get an owner-settable config is an open decision for the owner, and it is cheap today". Meanwhile LOOP.md:89-92 already specifies NINE of the numbers below changing after the first ten testers answer, and LOOP.md:127-130 says to deploy nothing before they do. A v3 that froze them would be obsolete on the day it shipped: that rebalance would cost a v4 plus a second migration. So the numbers LOOP.md names are tunable, inside bands, in one direction only, and FreezeRules below makes the frozen outcome reachable too. WHY THE RISK IS SMALL, RESTATED NOW THAT GNOMES HAVE A PRICE. An earlier version of this paragraph said "this realm holds nothing of value"; with a sale function that is no longer the whole truth, so here is the exact truth instead. The realm holds no coins between transactions: Buy's banker is BankerTypeOriginSend, which can move at most the coins the buyer attached to that one call and dies with the call, and there is no RealmSend banker anywhere in this file. Gnomes DO now have a price, and the admin reaches none of them: no function here lets adminAddress list, cancel, buy, move, burn or re-price a gnome, change the fee, change the recipient, or pause sales. The only state the admin can reach is ruleValues, through SetRule, inside the bands, one way. There is still no theft available to a compromised admin; what it can do is rebalance the game toward the player. THE SAFETY PROPERTY, STATED EXACTLY, BECAUSE AN EARLIER AND LOOSER VERSION OF THIS COMMENT WAS FALSE AND SHIPPED NOWHERE ONLY BECAUSE AN AUDIT CAUGHT IT. Two parts, and the second part is the one that was missing: 1. Every band is one-directional: kindEnd records which end is kinder to the player, and each default sits at the HOSTILE end, so the only place a value can go is toward the player. TestEveryBandIsPlayerSafe pins that, per key. 2. SetRule RATCHETS: it rejects any value less kind than the CURRENT one, not merely any value outside the band. Without this, a band being a closed interval meant it was freely re-enterable from either side -- levelBase 50 -> 30 -> 50 was permitted, and because levelFromXp is recomputed and written back on every SaveDay, that last call made STORED LEVELS DROP on gnomes that already existed. See SetRule's own comment for why that specific power was the one thing a redeploy could never grant, and for the price the ratchet charges (there are no take-backs). Together: for this realm's whole life, a level can never go down, a meter can never drain faster, income can never fall, a cost can never rise and a gate can never tighten. That is an auditable property of this table plus that check, not a promise about anybody's intentions. TestTuningIsAOneWayRatchetTowardThePlayer exercises both directions of every key, which is the test the earlier version of this comment did not have. THE ONE RESIDUAL, NAMED RATHER THAN CLAIMED AWAY. moodNow is first-match-wins (hungry, sulky, happy, content) and only happy and sulky carry an XP modifier, so BEING HUNGRY SHIELDS A NEGLECTED GNOME FROM THE SULKY PENALTY. feedBelly and bellyDropEveryHours both raise effective belly, so either can lift a gnome with belly 1-2, unpatted for 72h, out of hungry and into sulky -- costing it sulkyPenaltyPct of that one day's capped care XP. It is bounded at 10% of at most 60 XP, it only touches a gnome already being neglected for three days, and TestRaisingFeedBellyCanCostANeglectedGnomeTenPercent demonstrates it rather than denying it. Both keys stay tunable because they are the core of LOOP.md's rebalance and the kindest changes available; hungryAtOrBelow, which was the largest contributor and is on nobody's list, was removed from this table instead. THE OWNER CAN CLOSE THE RESIDUAL ENTIRELY IN ONE CALL: SetRule(sulkyPenaltyPct, 0) is permitted by the ratchet and makes the flip cost exactly nothing. The perverse incentive underneath it (a gnome at belly 2 out- earning the same gnome at belly 3) exists at the DEFAULT config too and is a faithful port of app/js/rules.js's own ladder -- correcting it here would create precisely the chain/browser mood divergence this realm exists to stop, so it is ported, not fixed.

Open
OID
02488d…6d24:10
ruleBands details (len=11)

+11 more Inspect slice

plot

type

plot is one of this gnome's numPlots garden plots. TWO DELIBERATE DIFFERENCES FROM v2's plotState (garden/v2/garden.gno:164-167): 1. No \`growing bool\`. plantedAt == 0 IS the empty state. v2 carried both and had to keep them consistent by hand in four places; one field cannot disagree with itself. 2. readyAt is STORED, not computed. This is the only derived value persisted anywhere in this realm and it is deliberate: growHours is tunable, so a readiness check against TODAY's growHours would silently change the ready time of a plot already in the ground. Freezing it at plant time means a rule change can never reach backwards into a plant a player has already made. 8 bytes per plot to make a tunable parameter safe.

Value

v4.plot

gnome

type

gnome is the whole persisted pet. EVERY FIELD HERE HAS AN EXPORTED READER -- see the package doc's one rule, and the table in DESIGN.md section 2.4 mapping field to reader. NOT here, on purpose: acorns. They live in \`balances\`, keyed by owner address, because ECONOMY.md:65-67 records the owner finding that hole himself -- "Acorns never travel with a gnome when it is sold. They belong to the player. Without this, money buys a gnome and inherits its acorns ... it is the kind of thing that is unfixable after deployment." Gnomes sell here, so this is no longer a theoretical distinction: Buy moves the gnome and no acorn moves to the buyer, because the purse was never on the gnome to begin with. The one acorn write Buy makes is TO THE SELLER -- it settles the garden's planted plots into the seller's own purse, because a planted plot is a claim on acorns and would otherwise have travelled. Meters are stored as (last-known value, the unix second it was set) per the brief, and meterNow works the current value out from elapsed time whenever anything reads it. Nothing ticks on this realm's clock, there is no scheduled job, and nothing here is written by anything other than a call the owning address itself signed.

Value

v4.gnome

listing

type

listing is one gnome offered for sale. Keyed by tokenID in \`listings\`; at most one per gnome, and List replaces rather than stacks. buyer is the zero address for "anyone may buy", or one named address. A named buyer is the ONLY gift-shaped path in this realm, and it still pays the floor: see Buy.

Value

v4.listing

feeConfig

type

feeConfig is the three sale constants, as init captured them. See the top of the file.

Value

v4.feeConfig

nextID

v0.ID

Value

<zero>

ownerIndex

*v0.Tree

// owner address -> tokenID

Open
OID
02488d…6d24:29
ownerIndex details

Inspect pointer

balances

*v0.Tree

// owner address -> int64 acorns (per PLAYER, not per gnome)

Open
OID
02488d…6d24:32
balances details

Inspect pointer

rulesFrozen

bool

Value

false

transferGate

string

Value

""

assertFeeConfig

func(cfg v4.feeConfig, realmAddr .uverse.address)

assertFeeConfig is init's gate on the three sale constants, split out so a test can prove what it refuses, and prove that the committed constants pass it, without a failing init taking the whole test package down. Every message names the constant to edit. realmAddr is this realm's own address: fees sent there would be burned, because nothing here can ever withdraw them.

Open
OID
02488d…6d24:49
assertFeeConfig details

Inspect func

saleGateExtension

type

saleGateExtension replaces v1/v2's soulboundExtension. v2 panicked on EVERY transfer; this panics on every transfer EXCEPT the one Buy is performing right now, identified by transferGate holding that exact token id. The grc721 PrivateLedger is never exported and no Approve, SetApprovalForAll or TransferFrom is exposed by this realm, so Buy is the only code that can reach the ledger at all -- the gate is the second lock on a door that already has one, and it is the lock whose claim ships on chain where anyone can read it. See the package doc for why this relies on the transaction boundary rather than on hook ordering: the hook fires AFTER the ledger write, so a refused transfer's write is discarded only because the uncaught panic aborts the whole transaction. No recover(), ever.

Value

v4.saleGateExtension

Adopt

func(name string, hat string, coat string, eyes int64, nose int64)

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.

Open
OID
02488d…6d24:50
Adopt details

Inspect func

faceKey

func(eyes int64, nose int64) string

faceKey / faceCount: the census tree's key and its zero-default read.

Open
OID
02488d…6d24:51
faceKey details

Inspect func

faceCount

func(eyes int64, nose int64) int64
Open
OID
02488d…6d24:52
faceCount details

Inspect func

validEyes

func(id int64) bool

validEyes / validNose: the whole face allowlist. A range check rather than a map because the ids are positions in a catalogue (see numEyes), and a range cannot have a hole in it.

Open
OID
02488d…6d24:53
validEyes details

Inspect func

alreadyAdopted

func(addr .uverse.address) bool

alreadyAdopted: unchanged from v1/v2, and now also Buy's "one gnome per address" check. Both ownerIndex and token.BalanceOf are consulted, not either alone, so a disagreement between this realm's own index and the grc721 ledger can never be used to mint or buy a second gnome to one address IN THIS REALM. Buy keeps the two in step by writing ownerIndex in the same call as the ledger move. "ONE GNOME PER ADDRESS" IS TRUE PER REALM AND FALSE GLOBALLY, AND THAT IS PERMANENT. This paragraph is here rather than only in DESIGN.md because DESIGN.md is a repo file: editable, not deployed, and not available to whoever reads this realm's source off chain with \`vm/qfile\`. The caveat that most needs to reach a future realm's author was otherwise written in the one place that does not ship. garden/v2 is deployed and immutable at gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v2. It keeps its OWN ownerIndex and its OWN grc721 ledger, neither of which this function can see, and it stays fully callable forever -- there is no pause and no admin anywhere in it. garden/v3 (block 336496) is deployed and immutable in the same way, with the one difference that nobody ever adopted on it, so it stands empty but callable. So one address can hold a v2 gnome AND a v3 gnome AND a v4 gnome simultaneously, with different names, faces, levels, callings and acorn balances, and each realm's own SaveDay keeps accruing on its own gnome indefinitely. Nothing on chain reconciles them and nothing ever can. The consequence that matters most to a future author: the deployed arena/v1 imports garden/v2 by absolute path, immutably, and reads GnomeByAddress live at battle-resolution time. For a dual adopter it therefore authoritatively reads the V2 gnome -- the state the app has stopped showing and this realm can never update. ChooseType here cannot touch it. ANY FUTURE REALM THAT ASSUMES A GLOBAL ONE-GNOME-PER-ADDRESS INVARIANT IS BUILDING ON A FALSE PREMISE. An arena/v2 written against v3 must pick one realm as the identity of record and say which. v3 deliberately does not import v2 to close this. See DESIGN.md section 6.3 for the full argument; the part of it that does not expire is that a hard import would pin this realm to one path on one chain and make it undeployable anywhere v2 is not -- the exact coupling arena/v1's own gnomod.toml has to warn its deployers about.

Open
OID
02488d…6d24:55
alreadyAdopted details

Inspect func

saveAction

type

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

Value

v4.saveAction

SaveDay

func(actionsRaw string)

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

Open
OID
02488d…6d24:56
SaveDay details

Inspect func

ChooseType

func(kind string)

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).

Open
OID
02488d…6d24:57
ChooseType details

Inspect func

List

func(priceUgnot int64, buyer .uverse.address, days int64)

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.

Open
OID
02488d…6d24:58
List details

Inspect func

Cancel

func()

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.

Open
OID
02488d…6d24:59
Cancel details

Inspect func

Buy

func(tokenID string)

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.

Open
OID
02488d…6d24:60
Buy details

Inspect func

Listing

func(tokenID string) (seller .uverse.address, priceUgnot int64, buyer .uverse.address, listedAtUnix int64, expiresAtUnix int64, feeUgnot int64, totalUgnot int64, found bool)

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.

Open
OID
02488d…6d24:61
Listing details

Inspect func

TotalListings

func() int64

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.

Open
OID
02488d…6d24:62
TotalListings details

Inspect func

ListingsPage

func(offset int, limit int) []string
Open
OID
02488d…6d24:63
ListingsPage details

Inspect func

QuoteFee

func(priceUgnot int64) (feeUgnot int64, totalUgnot int64)

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

Open
OID
02488d…6d24:64
QuoteFee details

Inspect func

FeeRules

func() string

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.

Open
OID
02488d…6d24:65
FeeRules details

Inspect func

Provenance

func(addr .uverse.address) (adoptedAtUnix int64, acquiredAtUnix int64, saleCount int64, found bool)

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

Open
OID
02488d…6d24:66
Provenance details

Inspect func

Face

func(addr .uverse.address) (eyes int64, nose int64, found bool)

Face is the reader for the two permanent traits v4 adds, by owner address -- the shape every other per-gnome read here has, so a client that already resolves an address keeps doing so. GnomeByAddress is NOT extended to carry them: its signature is byte-identical to v2's so that arena/v1's call and the app's positional tuple parser repoint with an import change, and a face is not something a battle or a village roster needs. found is false for an address with no gnome; eyes and nose are then 0, which is also a legal face, so check found.

Open
OID
02488d…6d24:67
Face details

Inspect func

FaceCount

func(eyes int64, nose int64) int64

FaceCount is how many gnomes have ever been minted with this exact face -- the population frequency ECONOMY.md's rarity argument rests on, read off the chain rather than rebuilt by a client. Out-of-range ids are refused rather than answered 0, because 0 would be a true statement about a face that cannot exist and a client could mistake it for "rarest".

Open
OID
02488d…6d24:68
FaceCount details

Inspect func

FaceCensus

func() string

FaceCensus is every face's count in one read: "eyes:nose=count;..." for every face with at least one gnome, eyes-major, noses-minor, so a client can draw the whole rarity table from one qeval. Empty while nobody has adopted. At most numEyes\*numNoses entries, so the cost is bounded by the catalogue and not by the population.

Open
OID
02488d…6d24:69
FaceCensus details

Inspect func

FaceOfToken

func(tokenID string) (eyes int64, nose int64, found bool)

FaceOfToken is the same read keyed by tokenID, because the one place a face is looked up without an owner in hand is the market: ListingsPage returns tokenIDs, and a buyer deciding between two listings should not need OwnerOfToken first. The face is the gnome's, not the owner's, so the tokenID is in fact the more honest key.

Open
OID
02488d…6d24:70
FaceOfToken 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 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.

Open
OID
02488d…6d24:71
GnomeByAddress details

Inspect func

GardenState

func(addr .uverse.address) (plantedAtCsv string, readyAtCsv string, readyCount int64, found bool)

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.

Open
OID
02488d…6d24:72
GardenState details

Inspect func

CareState

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

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.

Open
OID
02488d…6d24:73
CareState details

Inspect func

MeterState

func(addr .uverse.address) (belly int64, bellyAtUnix int64, cheer int64, cheerAtUnix int64, found bool)

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.

Open
OID
02488d…6d24:74
MeterState details

Inspect func

Progress

func(addr .uverse.address) (xpGarden int64, xpTinker int64, harvestCount int64, lastTinkerAtUnix int64, caredDayCount int64, dailyCareXpSpent int64, dailyCareDay int64, found bool)

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".

Open
OID
02488d…6d24:75
Progress details

Inspect func

CaredDays

func(addr .uverse.address) (daysCsv string, found bool)

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.

Open
OID
02488d…6d24:76
CaredDays details

Inspect func

Mood

func(addr .uverse.address) (mood string, found bool)

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.

Open
OID
02488d…6d24:77
Mood details

Inspect func

CollectionInfo

func() (name string, symbol string)

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.

Open
OID
02488d…6d24:78
CollectionInfo details

Inspect func

BalanceOf

func(addr .uverse.address) int64

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.

Open
OID
02488d…6d24:80
BalanceOf details

Inspect func

OwnerOfToken

func(tokenID string) (owner .uverse.address, found bool)
Open
OID
02488d…6d24:81
OwnerOfToken details

Inspect func

TotalGnomes

func() int64

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.

Open
OID
02488d…6d24:82
TotalGnomes details

Inspect func

GnomesPage

func(offset int, limit int) []string

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.

Open
OID
02488d…6d24:83
GnomesPage details

Inspect func

Acorns

func(addr .uverse.address) (acorns int64, found bool)

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.

Open
OID
02488d…6d24:84
Acorns details

Inspect func

Rules

func() string

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.

Open
OID
02488d…6d24:85
Rules details

Inspect func

RuleBands

func() string

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.

Open
OID
02488d…6d24:86
RuleBands details

Inspect func

AdminState

func() (admin .uverse.address, frozen bool)
Open
OID
02488d…6d24:87
AdminState details

Inspect func

SetRule

func(key string, value int64)

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.

Open
OID
02488d…6d24:88
SetRule details

Inspect func

FreezeRules

func()

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.

Open
OID
02488d…6d24:89
FreezeRules details

Inspect func

Render

func(path string) string

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.

Open
OID
02488d…6d24:90
Render details

Inspect func

gnomeForAddr

func(addr .uverse.address) (tid string, g *v4.gnome, found bool)
Open
OID
02488d…6d24:91
gnomeForAddr details

Inspect func

lowerASCII

func(s string) string

lowerASCII folds A-Z to a-z and leaves every other byte alone. Used on a typed address before validation (List's reserved buyer, Render's path): bech32's case-insensitivity is an ASCII rule, so this is exactly the fold the specification licenses and no more. A non-ASCII byte is left for address.IsValid() to reject.

Open
OID
02488d…6d24:92
lowerASCII details

Inspect func

anyPlotPlanted

func(g *v4.gnome) bool

anyPlotPlanted is the reserve rule's "is income on its way" test: a planted plot, ready or growing, is a claim on at least plantCost acorns (Buy's settlement makes that literal), so a purse below the reserve is survivable while one exists.

Open
OID
02488d…6d24:93
anyPlotPlanted details

Inspect func

balanceOf

func(addr .uverse.address) int64

balanceOf: a missing key is a zero balance, not an error. Only Adopt creates a key, so a missing key means "no gnome here", which every caller has already checked.

Open
OID
02488d…6d24:94
balanceOf details

Inspect func

setBalance

func(addr .uverse.address, v int64)

setBalance refuses to store a negative balance. Nothing above should ever produce one -- every spend is guarded by its own "not enough acorns" check -- so this is not a guard that is expected to fire; it is a guard that makes a future arithmetic mistake abort the transaction instead of silently minting a debt the economy has no concept of.

Open
OID
02488d…6d24:95
setBalance details

Inspect func

grantStartingPurseIfNew

func(addr .uverse.address)

grantStartingPurseIfNew gives an address acornsStart the FIRST time a gnome arrives in its hands, by Adopt or by Buy, and never again. See Adopt's comment: a seller keeps their purse, so neither re-adopting nor buying may refill it.

Open
OID
02488d…6d24:96
grantStartingPurseIfNew details

Inspect func

feeFor

func(priceUgnot int64) int64

feeFor is the whole fee formula: floor + price \* rate / 10000, rounded down. Written as (price/10000)\*rate + ((price%10000)\*rate)/10000 rather than price\*rate/10000. The two are identical for every input (write price = 10000q + r; then floor(price\*rate/ 10000) = q\*rate + floor(r\*rate/10000) because q\*rate is an integer), but the first form cannot overflow int64 for any legal input: price \<= maxPriceUgnot = 10^15 and rate \<= maxFeeRateBps = 10^4, so (price/10000)\*rate \<= 10^15 and the remainder term is \< 10^4 \* 10^4. The naive form would overflow at price\*rate > 9.2\*10^18, i.e. a 10^15 price at any rate above 9223 bps. TestFeeArithmeticMatchesTheNaiveFormula pins the identity.

Open
OID
02488d…6d24:97
feeFor details

Inspect func

exactUgnot

func(send chain.Coins, want int64) bool

exactUgnot is Buy's payment check: one coin, in ugnot, of exactly want. Not "at least".

Open
OID
02488d…6d24:98
exactUgnot details

Inspect func

coinsString

func(c chain.Coins) string

coinsString renders an envelope for a refusal message; "nothing" for an empty one.

Open
OID
02488d…6d24:99
coinsString details

Inspect func

listingFor

func(tokenID string) (*v4.listing, bool)
Open
OID
02488d…6d24:100
listingFor details

Inspect func

rule

func(key string) int64

rule reads a tunable. A missing key is a bug in this file, not a runtime condition, so it panics rather than returning a silent zero -- a zero growHours or a zero levelBase would be a far worse outcome than an aborted call.

Open
OID
02488d…6d24:101
rule details

Inspect func

bandFor

func(key string) (v4.ruleBand, bool)
Open
OID
02488d…6d24:102
bandFor details

Inspect func

validNameLength

func(name string) bool

validNameLength/validNameCharset/hasVisibleNameContent/validNameRune/invisibleLetterRunes: unchanged from v1 and v2, including v2's audit fix F3 (\\uXXXX escapes rather than literal invisible glyphs, so the denylist is readable in an editor and cannot be silently emptied by a formatter that strips default-ignorable characters). See v1's own comments for the exhaustive scan proving the list complete against this toolchain's Unicode version, and realms/garden/NOTES.md for the open product question about combining marks -- that allowlist excludes every combining mark, which makes zalgo names structurally impossible and also makes NFD Latin, Devanagari, Thai, vocalised Hebrew and Arabic unspellable. That trade-off is recorded as the owner's to revisit and is carried forward here unchanged rather than silently re-decided.

Open
OID
02488d…6d24:103
validNameLength details

Inspect func

validNameCharset

func(name string) bool
Open
OID
02488d…6d24:104
validNameCharset details

Inspect func

hasVisibleNameContent

func(name string) bool
Open
OID
02488d…6d24:105
hasVisibleNameContent details

Inspect func

invisibleLetterRunes

map[int32]bool
Open
OID
02488d…6d24:107
invisibleLetterRunes details

Inspect map

validNameRune

func(r int32) bool
Open
OID
02488d…6d24:108
validNameRune details

Inspect func

quoteAction

func(part string) string

parseActions parses and validates SaveDay's wire format, enforcing maxActionsPerCall on the delimiter COUNT before strings.Split ever runs (v2's audit fix F5: Split allocates the full result slice before any length check could inspect it, so a megabyte of semicolons allocated half a million short-lived strings before being rejected. strings.Count scans the raw bytes without allocating a result, so the oversized case now costs nothing proportional to the input. The caller always paid their own gas for this, so it was never a cross-user DoS -- the fix makes the claim true, it does not close a vulnerability). quoteAction renders an offending entry for a panic message, TRUNCATED. Without this, the four malformed-action panics below interpolated the caller's own bytes at full length, so a semicolon-free, colon-free megabyte passed both delimiter guards and then built a megabyte-long panic string -- one remaining path that allocated proportional to the input, which is precisely what this function's own comment claims cannot happen. Harmless (about 2x amplification of data already in the transaction, entirely caller-paid, and a transaction abort message is not a markdown sink) but it made the claim false, and the claim ships. 48 bytes is comfortably longer than any legal entry -- "harvest:2:1791600000" is 20 -- so a real player's error message is never truncated and only an abusive one is.

Open
OID
02488d…6d24:109
quoteAction details

Inspect func

parseActions

func(raw string) []v4.saveAction
Open
OID
02488d…6d24:110
parseActions details

Inspect func

parseUnixField

func(s string, part string) int64
Open
OID
02488d…6d24:111
parseUnixField details

Inspect func

parsePlotField

func(s string, part string) int
Open
OID
02488d…6d24:112
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 with no timezone handling, because unix time is UTC by definition.

Open
OID
02488d…6d24:113
dayNumber details

Inspect func

recordCaredDay

func(g *v4.gnome, day int64)

recordCaredDay folds one cared UTC day into the gnome's lifetime count and its 64-day window. See the gnome struct's own comment for the storage shape and DESIGN.md section 2.3 for why the chain records days and never computes the streak. This handles a day arriving AT OR BEFORE lastCaredDay even though SaveDay's replay rule (every action strictly after lastActionAt) makes that unreachable today. Two extra lines to make this function correct on its own terms rather than correct only because of an invariant enforced two hundred lines away. On a contract that can never be edited, a total function is worth more than the lines it costs -- and a reader of this function alone should not have to go and verify SaveDay's ordering rule to know it is sound.

Open
OID
02488d…6d24:114
recordCaredDay details

Inspect func

caredDaysCsv

func(g *v4.gnome) string

caredDaysCsv decodes the window, newest first.

Open
OID
02488d…6d24:115
caredDaysCsv details

Inspect func

meterNow

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

meterNow ports app/js/rules.js's meterNow exactly: work the current value out from a last-known value, the unix second it was set, and how many whole dropEveryHours periods have elapsed since, clamped to \[0, meterMax]. A tunable decay rate re-derives history under the new rate, which is harmless precisely because meters saturate: the affected span is bounded in practice by the 48-hour backfill window, and a long-dormant gnome's belly is already clamped at 0 under either rate. See DESIGN.md section 5.4.

Open
OID
02488d…6d24:116
meterNow details

Inspect func

clampMeter

func(v int64) int64
Open
OID
02488d…6d24:117
clampMeter details

Inspect func

canPat

func(lastPatAt int64, now int64, cooldownHours int64) bool

canPat ports app/js/rules.js's canPat. lastPatAt == 0 means never patted; see the gnome struct's comment for why 0 is an unambiguous sentinel here and is not in the JS.

Open
OID
02488d…6d24:118
canPat details

Inspect func

moodNow

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

moodNow ports app/js/rules.js's CURRENT moodNow, in brief table 3.3's first-match-wins order: hungry, then sulky, then happy, then content. "Asleep" is display-only and has no on-chain representation, exactly as in rules.js. THE adoptedAt FLOOR IS THE FIX FOR A DIVERGENCE v2 CANNOT BE FIXED FOR, and it is the reason this function takes a parameter v2's did not. v2 ported rules.js's mood as it then stood, where a never-patted gnome's "last attention" fell back to the unix epoch, so "no pat for 72 hours" was trivially true and every brand-new gnome read as sulky -- v2's own comment flags this and notes rules.test.js never exercised it. rules.js:103-119 has since been fixed, for a reported bug: "EVERY newly adopted gnome was instantly sulky" and "the very first thing a new player read was '\<name>'s in a huff'". v2 cannot be edited, so the deployed realm and the shipped browser now disagree about the mood of the same gnome, and the chain's answer is the one that applies the XP penalty. This matches the fixed version: neverPatted -> floor "last attention" at adoptedAt, because a gnome has been cared for, by definition, at the moment somebody chose it. rules.js also maxes in lastVisitReceivedAt. This realm has no visit mechanic (DESIGN.md section 7.1), so that input is permanently absent, and rules.js with lastVisitReceivedAt === null gives the identical answer. The floor applies ONLY to a gnome that has genuinely never been patted: rules.js's own comment records that an earlier attempt applied it unconditionally and quietly made neglect unreachable, so a gnome last patted 73 hours ago still sulks here, as it should. hungryThreshold is a PARAMETER rather than a package-constant reference inside the body, so this function is pure in its arguments and its formula can be checked against rules.js without knowing any realm state. It is deliberately NOT named hungryAtOrBelow: that is the name of the package constant every caller passes, and shadowing it here would leave a reader of this body alone unable to tell whether the threshold is fixed or caller-supplied -- on a file whose stated standard is that reading one function should be enough. The older note on why the parameter exists at all: the threshold was once a rule(...) lookup inside the body, so this function is pure in its arguments. It used to read the rule itself while SaveDay had already cached that same value for the planting gate -- harmless today, but it made the answer depend on realm state the signature did not mention, which is exactly the kind of hidden input that makes a formula hard to check against rules.js.

Open
OID
02488d…6d24:119
moodNow details

Inspect func

xpToNextWith

func(level int64, base int64, step int64) int64

xpToNext/totalXpForLevel/levelFromXp port app/js/rules.js's functions of the same name: levelBase + levelStep\*level to reach the next level, maxLevel the ceiling. levelBase and levelStep are tunable, so a level is recomputed from totalXp on every save -- the bands only permit them to fall, so a level can rise from a tune and can never drop. arena/v1 reads this level for ranked eligibility, so a tune can make a gnome newly eligible and never newly ineligible. DESIGN.md section 5.4. xpToNextWith/totalXpForLevelWith take the curve's two numbers as ARGUMENTS rather than reading them per call, and levelFromXp below reads them exactly once. This is not style. The obvious shape -- xpToNext() reading rule(keyLevelBase) and rule(keyLevelStep) itself -- puts two avl lookups inside a loop that is inside another loop bounded by maxLevel, so one SaveDay paid on the order of 800 tree reads to work out a level that needs two numbers and some arithmetic. garden/v2 had the numbers as compile-time constants and paid nothing; making them tunable must not quietly cost that.

Open
OID
02488d…6d24:120
xpToNextWith details

Inspect func

totalXpForLevelWith

func(level int64, base int64, step int64) int64
Open
OID
02488d…6d24:121
totalXpForLevelWith details

Inspect func

xpToNext

func(level int64) int64

xpToNext/totalXpForLevel are the single-argument forms the tests and any future reader want, kept so the curve can be asked about without threading the two numbers by hand.

Open
OID
02488d…6d24:122
xpToNext details

Inspect func

totalXpForLevel

func(level int64) int64
Open
OID
02488d…6d24:123
totalXpForLevel details

Inspect func

levelFromXp

func(totalXp int64) int64
Open
OID
02488d…6d24:124
levelFromXp details

Inspect func
saleFeeRecipient : untyped string ="g1ylpe9juk8cafgf9rt09wzuz85ug2lyfk5metyv"
saleFeeRateBps : int64 =500
saleFeeFloorUgnot : int64 =1000000
collectionName : untyped string ="Gnotchi gnomes"
collectionSymbol : untyped string ="GNOME"
adminAddress : untyped string ="g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar"
maxNameLen : untyped bigint =(24 <untyped> bigint)
numEyes : int64 =10
numNoses : int64 =8
acornReserve : int64 =2
hourSeconds : int64 =3600
daySeconds : int64 =86400
numPlots : untyped bigint =(3 <untyped> bigint)
plantCost : int64 =1
feedCost : int64 =1
acornsStart : int64 =10
meterMax : int64 =10
patCheer : int64 =3
hungryAtOrBelow : int64 =2
happyBonusPct : int64 =10
happyWindowHours : int64 =2
sulkyAfterHours : int64 =72
maxLevel : int64 =20
typeAtLevel : int64 =5
xpPat : int64 =5
xpFeed : int64 =5
xpHarvestPerPlot : int64 =10
xpTinkerTask : int64 =15
maxActionsPerCall : untyped bigint =(32 <untyped> bigint)
maxBackfillHours : int64 =48
caredWindowDays : uint =64
maxGnomesPageSize : untyped bigint =(100 <untyped> bigint)
ugnotDenom : untyped string ="ugnot"
maxPriceUgnot : int64 =1000000000000000
maxFeeRateBps : int64 =10000
maxListingDays : int64 =30
maxListingsPageSize : untyped bigint =(100 <untyped> bigint)
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"
typeGardener : untyped string ="gardener"
typeExplorer : untyped string ="explorer"
typeTinkerer : untyped string ="tinkerer"
keyGrowHours : untyped string ="growHours"
keyHarvestPerPlot : untyped string ="harvestPerPlot"
keyFeedBelly : untyped string ="feedBelly"
keyBellyDrop : untyped string ="bellyDropEveryHours"
keyCheerDrop : untyped string ="cheerDropEveryHours"
keyPatCooldown : untyped string ="patCooldownHours"
keyTinkerCost : untyped string ="tinkerCost"
keyDailyCareCap : untyped string ="dailyCareCap"
keyLevelBase : untyped string ="levelBase"
keyLevelStep : untyped string ="levelStep"
keySulkyPenaltyPct : untyped string ="sulkyPenaltyPct"
kindMin : untyped string ="min"
kindMax : untyped string ="max"
validHats : map[string]bool Inspect
validCoats : map[string]bool Inspect
validTypes : map[string]bool Inspect
ruleBand : type =v4.ruleBand
ruleBands : []v4.ruleBand (len=11) Inspect
plot : type =v4.plot
gnome : type =v4.gnome
listing : type =v4.listing
feeConfig : type =v4.feeConfig
token : *v0.Token Inspect
ledger : *v0.PrivateLedger Inspect
nextID : v0.ID =<zero>
gnomes : *v0.Tree Inspect
ownerIndex : *v0.Tree Inspect
balances : *v0.Tree Inspect
ruleValues : *v0.Tree Inspect
listings : *v0.Tree Inspect
faceCounts : *v0.Tree Inspect
rulesFrozen : bool =false
fee : v4.feeConfig Inspect
transferGate : string =""
init.83 : func() Inspect
assertFeeConfig : func(cfg v4.feeConfig, realmAddr .uverse.address) Inspect
saleGateExtension : type =v4.saleGateExtension
Adopt : func(name string, hat string, coat string, eyes int64, nose int64) Inspect
faceKey : func(eyes int64, nose int64) string Inspect
faceCount : func(eyes int64, nose int64) int64 Inspect
validEyes : func(id int64) bool Inspect
validNose : func(id int64) bool Inspect
alreadyAdopted : func(addr .uverse.address) bool Inspect
saveAction : type =v4.saveAction
SaveDay : func(actionsRaw string) Inspect
ChooseType : func(kind string) Inspect
List : func(priceUgnot int64, buyer .uverse.address, days int64) Inspect
Cancel : func() Inspect
Buy : func(tokenID string) Inspect
Listing : func(tokenID string) (seller .uverse.address, priceUgnot int64, buyer .uverse.address, listedAtUnix int64, expiresAtUnix int64, feeUgnot int64, totalUgnot int64, found bool) Inspect
TotalListings : func() int64 Inspect
ListingsPage : func(offset int, limit int) []string Inspect
QuoteFee : func(priceUgnot int64) (feeUgnot int64, totalUgnot int64) Inspect
FeeRules : func() string Inspect
Provenance : func(addr .uverse.address) (adoptedAtUnix int64, acquiredAtUnix int64, saleCount int64, found bool) Inspect
Face : func(addr .uverse.address) (eyes int64, nose int64, found bool) Inspect
FaceCount : func(eyes int64, nose int64) int64 Inspect
FaceCensus : func() string Inspect
FaceOfToken : func(tokenID string) (eyes int64, nose int64, 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
GardenState : func(addr .uverse.address) (plantedAtCsv string, readyAtCsv string, readyCount int64, found bool) Inspect
CareState : func(addr .uverse.address) (belly int64, cheer int64, acorns int64, totalXp int64, lastPatAtUnix int64, lastHappyEventAtUnix int64, lastActionAtUnix int64, found bool) Inspect
MeterState : func(addr .uverse.address) (belly int64, bellyAtUnix int64, cheer int64, cheerAtUnix int64, found bool) Inspect
Progress : func(addr .uverse.address) (xpGarden int64, xpTinker int64, harvestCount int64, lastTinkerAtUnix int64, caredDayCount int64, dailyCareXpSpent int64, dailyCareDay int64, found bool) Inspect
CaredDays : func(addr .uverse.address) (daysCsv string, found bool) Inspect
Mood : func(addr .uverse.address) (mood string, found bool) Inspect
CollectionInfo : func() (name string, symbol string) Inspect
TotalSupply : func() int64 Inspect
BalanceOf : func(addr .uverse.address) int64 Inspect
OwnerOfToken : func(tokenID string) (owner .uverse.address, found bool) Inspect
TotalGnomes : func() int64 Inspect
GnomesPage : func(offset int, limit int) []string Inspect
Acorns : func(addr .uverse.address) (acorns int64, found bool) Inspect
Rules : func() string Inspect
RuleBands : func() string Inspect
AdminState : func() (admin .uverse.address, frozen bool) Inspect
SetRule : func(key string, value int64) Inspect
FreezeRules : func() Inspect
Render : func(path string) string Inspect
gnomeForAddr : func(addr .uverse.address) (tid string, g *v4.gnome, found bool) Inspect
lowerASCII : func(s string) string Inspect
anyPlotPlanted : func(g *v4.gnome) bool Inspect
balanceOf : func(addr .uverse.address) int64 Inspect
setBalance : func(addr .uverse.address, v int64) Inspect
grantStartingPurseIfNew : func(addr .uverse.address) Inspect
feeFor : func(priceUgnot int64) int64 Inspect
exactUgnot : func(send chain.Coins, want int64) bool Inspect
coinsString : func(c chain.Coins) string Inspect
listingFor : func(tokenID string) (*v4.listing, bool) Inspect
rule : func(key string) int64 Inspect
bandFor : func(key string) (v4.ruleBand, 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
quoteAction : func(part string) string Inspect
parseActions : func(raw string) []v4.saveAction Inspect
parseUnixField : func(s string, part string) int64 Inspect
parsePlotField : func(s string, part string) int Inspect
dayNumber : func(unixSeconds int64) int64 Inspect
recordCaredDay : func(g *v4.gnome, day int64) Inspect
caredDaysCsv : func(g *v4.gnome) string 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, cooldownHours int64) bool Inspect
moodNow : func(belly int64, lastPatAt int64, lastHappyEventAt int64, adoptedAt int64, now int64, hungryThreshold int64) string Inspect
xpToNextWith : func(level int64, base int64, step int64) int64 Inspect
totalXpForLevelWith : func(level int64, base int64, step int64) int64 Inspect
xpToNext : func(level int64) int64 Inspect
totalXpForLevel : func(level int64) int64 Inspect
levelFromXp : func(totalXp int64) int64 Inspect