{"jsonrpc":"2.0","id":"","result":{"genesis":{"genesis_time":"2026-08-10T08:49:32.608725114Z","chain_id":"dev","consensus_params":{"Block":{"MaxTxBytes":"1000000","MaxDataBytes":"2000000","MaxBlockBytes":"0","MaxGas":"10000000000","TimeIotaMS":"100"},"Validator":{"PubKeyTypeURLs":["/tm.PubKeyEd25519"]}},"validators":[{"address":"g12lq3j2r5t66x46snwwg3hlgftgn06se7cmvdwd","pub_key":{"@type":"/tm.PubKeyEd25519","value":"/2r8bb7OZsp0iWiaSNeQTKUbjcEmQ8TEZEDvG9rqXCI="},"power":"10","name":"self"}],"app_hash":null,"app_state":{"balances":["g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=10000000000000ugnot"],"txs":[{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"txlink","path":"gno.land/p/moul/txlink","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/txlink\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"txlink.gno","body":"// Package txlink provides utilities for creating transaction-related links\n// compatible with Gnoweb, Gnobro, and other clients within the Gno ecosystem.\n//\n// This package is optimized for generating lightweight transaction links with\n// flexible arguments, allowing users to build dynamic links that integrate\n// seamlessly with various Gno clients.\n//\n// The package offers a way to generate clickable transaction MD links\n// for the current \"relative realm\":\n//\n//  Using a builder pattern for more structured URLs:\n//     txlink.NewLink(\"MyFunc\").\n//         AddArgs(\"k1\", \"v1\", \"k2\", \"v2\"). // or multiple at once\n//         SetSend(\"1000000ugnot\").\n//         URL()\n//\n// The builder pattern (TxBuilder) provides a fluent interface for constructing\n// transaction URLs in the current \"relative realm\". Like Call, it supports both\n// local realm paths and fully qualified paths through the underlying Call\n// implementation.\n//\n// The Call function remains the core implementation, used both directly and\n// internally by the builder pattern to generate the final URLs.\n//\n// This package is a streamlined alternative to helplink, providing similar\n// functionality for transaction links without the full feature set of helplink.\n\npackage txlink\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nvar chainDomain = runtime.ChainDomain()\n\n// Realm represents a specific realm for generating tx links.\ntype Realm string\n\n// TxBuilder provides a fluent interface for building transaction URLs\ntype TxBuilder struct {\n\tfn        string   // function name\n\targs      []string // key-value pairs\n\tsend      string   // optional send amount\n\trealm_XXX Realm    // realm for the URL\n}\n\n// NewLink creates a transaction link builder for the specified function in the current realm.\nfunc NewLink(fn string) *TxBuilder {\n\treturn Realm(\"\").NewLink(fn)\n}\n\n// NewLink creates a transaction link builder for the specified function in this realm.\nfunc (r Realm) NewLink(fn string) *TxBuilder {\n\tif fn == \"\" {\n\t\treturn nil\n\t}\n\treturn \u0026TxBuilder{fn: fn, realm_XXX: r}\n}\n\n// addArg adds a key-value argument pair. Returns the builder for chaining.\nfunc (b *TxBuilder) addArg(key, value string) *TxBuilder {\n\tif b == nil {\n\t\treturn nil\n\t}\n\tif key == \"\" {\n\t\treturn b\n\t}\n\n\t// Special case: \".\" prefix is for reserved keywords.\n\tif strings.HasPrefix(key, \".\") {\n\t\tpanic(\"invalid key\")\n\t}\n\n\tb.args = append(b.args, key, value)\n\treturn b\n}\n\n// AddArgs adds multiple key-value pairs at once. Arguments should be provided\n// as pairs: AddArgs(\"key1\", \"value1\", \"key2\", \"value2\").\nfunc (b *TxBuilder) AddArgs(args ...string) *TxBuilder {\n\tif b == nil {\n\t\treturn nil\n\t}\n\tif len(args)%2 != 0 {\n\t\tpanic(\"odd number of arguments\")\n\t}\n\t// Add key-value pairs\n\tfor i := 0; i \u003c len(args); i += 2 {\n\t\tkey := args[i]\n\t\tvalue := args[i+1]\n\t\tb.addArg(key, value)\n\t}\n\treturn b\n}\n\n// SetSend adds a send amount. (Only one send amount can be specified.)\nfunc (b *TxBuilder) SetSend(amount string) *TxBuilder {\n\tif b == nil {\n\t\treturn nil\n\t}\n\tif amount == \"\" {\n\t\treturn b\n\t}\n\tb.send = amount\n\treturn b\n}\n\n// URL generates the final URL using the standard $help\u0026func=name format.\nfunc (b *TxBuilder) URL() string {\n\tif b == nil || b.fn == \"\" {\n\t\treturn \"\"\n\t}\n\targs := b.args\n\tif b.send != \"\" {\n\t\targs = append(args, \".send\", b.send)\n\t}\n\treturn b.realm_XXX.Call(b.fn, args...)\n}\n\n// Call returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc Call(fn string, args ...string) string {\n\treturn Realm(\"\").Call(fn, args...)\n}\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\tcurPath := unsafe.CurrentRealm().PkgPath()\n\t\treturn strings.TrimPrefix(curPath, chainDomain)\n\t}\n\n\t// local realm -\u003e /realm\n\trlm := string(r)\n\tif strings.HasPrefix(rlm, chainDomain) {\n\t\treturn strings.TrimPrefix(rlm, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + string(r)\n}\n\n// Call returns a URL for the specified function with optional key-value\n// arguments.\nfunc (r Realm) Call(fn string, args ...string) string {\n\tif len(args) == 0 {\n\t\treturn r.prefix() + \"$help\u0026func=\" + fn\n\t}\n\n\t// Create url.Values to properly encode parameters.\n\t// But manage \u0026func=fn as a special case to keep it as the first argument.\n\tvalues := url.Values{}\n\n\t// Check if args length is even\n\tif len(args)%2 != 0 {\n\t\tpanic(\"odd number of arguments\")\n\t}\n\t// Add key-value pairs to values\n\tfor i := 0; i \u003c len(args); i += 2 {\n\t\tkey := args[i]\n\t\tvalue := args[i+1]\n\t\tvalues.Add(key, value)\n\t}\n\n\t// Build the base URL and append encoded query parameters\n\treturn r.prefix() + \"$help\u0026func=\" + fn + \"\u0026\" + values.Encode()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"bptree","path":"gno.land/p/nt/bptree/v0","files":[{"name":"PLAN.md","body":"# Mutable B+ Tree for Gno\n\n## Goal\n\nA mutable (in-place) B+ tree with the same API as `gno.land/p/nt/avl/v0`.\nNo merkle hashing, no versioning, no persistence — just a simple, efficient\nordered map with configurable fanout.\n\n## API\n\n```go\n// Constructors\nfunc NewBPTreeN(fanout int) *BPTree   // arbitrary fanout (minimum 4)\nfunc NewBPTree32() *BPTree            // convenience: fanout 32\n\n// ITree interface (same as avl.ITree)\ntype ITree interface {\n    Size() int\n    Has(key string) bool\n    Get(key string) any\n    GetByIndex(index int) (key string, value any)\n    Iterate(start, end string, cb IterCbFn) bool\n    ReverseIterate(start, end string, cb IterCbFn) bool\n    IterateByOffset(offset int, count int, cb IterCbFn) bool\n    ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool\n    Set(key string, value any) (updated bool)\n    Remove(key string) (value any, removed bool)\n}\n\ntype IterCbFn func(key string, value any) bool\n```\n\n`var _ ITree = (*BPTree)(nil)` enforces the interface at compile time.\n\n## Semantics (matching avl exactly)\n\n- `Set`: insert or update. Returns true if key already existed.\n- `Remove`: delete key. Returns (old value, true) if found.\n- `GetByIndex`: 0-based index into sorted keys. Panics on invalid index.\n- `Iterate(start, end, cb)`: ascending, start inclusive, end exclusive.\n  Empty string means no bound.\n- `ReverseIterate(start, end, cb)`: descending, start inclusive, end inclusive.\n  Empty string means no bound.\n- `IterateByOffset(offset, count, cb)`: ascending from the offset-th leaf entry\n  (0-indexed from the smallest key). Returns false if offset \u003e= size or count \u003c= 0.\n- `ReverseIterateByOffset(offset, count, cb)`: descending, where offset is\n  0-indexed from the largest key. offset=0 starts at the largest key,\n  offset=1 skips the largest and starts at the second-largest, etc.\n  Equivalent to: visit entries in descending order, skip `offset`, take `count`.\n- All iteration callbacks return true to stop early.\n\n## Node types\n\n### leafNode\n\n```go\ntype leafNode struct {\n    keys   []string  // sorted, len \u003c= fanout\n    values []*any    // parallel to keys\n}\n```\n\nLeaf nodes store all key-value data. No sibling pointers — iteration\nuses stack-based traversal to avoid ref-count \u003e= 2 (see\n[Ref-Count Safety](#ref-count-safety)).\n\nCapacity: up to `fanout` entries. Splits when full after insert.\nMinimum occupancy enforced during deletion: `fanout/2` (except the root leaf).\nNote: the 90/10 split optimization intentionally creates a right leaf with\nonly 2 entries, which may be below `fanout/2` for large fanouts. This is\nstandard B+ tree practice — if a subsequent Remove causes underflow, the\nnormal rebalance logic (redistribute or merge) handles it.\n\n### innerNode\n\n```go\ntype innerNode struct {\n    keys     []string // separator keys, len = len(children)-1\n    children []node   // child pointers, len \u003c= fanout\n    sizes    []int    // sizes[i] = total leaf count in children[i] subtree\n}\n```\n\n`keys[i]` = minimum key of `children[i+1]` (standard B+ tree convention).\nAn inner node with k separator keys has k+1 children.\n\nCapacity: up to `fanout` children (= `fanout-1` keys). Splits when full.\nMinimum occupancy: `fanout/2` children (except root, which may have as few as 2).\n\n### node interface\n\n```go\ntype node interface {\n    isLeaf() bool\n    nodeSize() int    // total leaf entries in subtree\n    minKey() string   // leftmost key in subtree\n}\n```\n\n`nodeSize()`: leafNode returns `len(keys)` (O(1)), innerNode sums `sizes[]` (O(fanout)).\n\nUsed internally so tree methods can handle both node types polymorphically.\nType assertions to `*leafNode` or `*innerNode` are used when accessing\ntype-specific fields (children, sizes).\n\n## BPTree struct\n\n```go\ntype BPTree struct {\n    root   node\n    size   int       // total number of key-value pairs\n    fanout int       // max children per inner node / max entries per leaf\n}\n```\n\nNo `first`/`last` pointers — they would create ref-count \u003e= 2 on leaves.\nFull-range iteration descends from the root (O(height) to find the first\nor last leaf, amortized O(1) per entry thereafter).\n\n## Key algorithms\n\n### Search (Get, Has)\n\nDescend from root. At each inner node, binary search `keys` to find the\nchild index. At the leaf, binary search `keys` for exact match.\n\n### Insert (Set)\n\n1. Descend root-to-leaf, recording the path (stack of `(innerNode, childIdx)` pairs).\n2. Binary search in the leaf for the key.\n3. If found: update value in place, return `updated=true`. No structural change.\n4. If not found: insert key-value at the sorted position.\n   - Increment `size` on the tree and all ancestor `sizes[childIdx]` entries.\n   - If the leaf now has `fanout+1` entries, **split**:\n     - **90/10 split** (append pattern): if the new key was inserted at\n       position `fanout` in the overflowed leaf (i.e., it is greater than\n       all pre-existing keys), split asymmetrically: left gets `fanout-1` entries, right\n       gets 2 entries (the last existing key + the new key). This keeps left\n       leaves ~97% full for sequential inserts.\n     - **50/50 split** (random pattern): otherwise, left gets `(fanout+1)/2`\n       (floor), right gets the rest.\n     - Create new right leaf node.\n     - Promote `right.keys[0]` as separator to the parent.\n     - Update parent's `sizes` for both children.\n     - If the parent now has `fanout+1` children, split the parent recursively.\n   - If the root splits, create a new inner root with 2 children.\n\n### Remove\n\n1. Descend root-to-leaf, recording the path.\n2. Binary search in the leaf for the key.\n3. If not found: return `(nil, false)`.\n4. If found: remove entry, decrement `size` and ancestor `sizes`.\n   - If the leaf is the root and now empty: set root to nil.\n   - If the leaf is the root and non-empty: done (root has no minimum).\n   - Otherwise, if leaf has fewer than `fanout/2` entries, **rebalance**:\n     - Try to **redistribute** from left sibling (if it has more than `fanout/2`).\n     - Try to **redistribute** from right sibling (if it has more than `fanout/2`).\n     - Otherwise **merge** with a sibling:\n       - Concatenate into the left node, remove the right child and its\n         separator from parent.\n       - Update parent's `sizes` and `size`.\n       - If parent is the root and drops to 1 child, replace root with that child.\n       - If parent is not the root and has fewer than `fanout/2` children,\n         rebalance recursively (the root is exempt — it may have as few as 2 children).\n   - If the minimum key was removed (pos == 0), update ancestor separator keys\n     before rebalancing. Rebalance operations also fix separators as needed.\n\n### Redistribute detail\n\n**Redistribute from left sibling to deficient child (both leaves):**\n1. Move left sibling's last key-value to the front of the deficient child.\n2. Update parent `keys[childIdx-1]` = deficient child's new `keys[0]` (its min key changed).\n3. Adjust parent `sizes`: decrement left's size, increment child's size.\n\n**Redistribute from right sibling to deficient child (both leaves):**\n1. Move right sibling's first key-value to the end of the deficient child.\n2. Update parent `keys[childIdx]` = right sibling's new `keys[0]` (its min key changed).\n3. Adjust parent `sizes`: decrement right's size, increment child's size.\n\n**Redistribute from left sibling to deficient child (both inner nodes):**\n1. Pull down parent's separator `keys[childIdx-1]` — **prepend** it to deficient child's keys\n   (insert at position 0, since the moved child goes to the front).\n2. Move left sibling's last child to the **front** of deficient child's children.\n   Move left sibling's last `sizes` entry to the front of deficient's sizes too.\n3. Push up left sibling's last key to replace parent's `keys[childIdx-1]`.\n4. Remove left sibling's last key, last child, and last size entry.\n5. Update parent's `sizes[childIdx-1]` and `sizes[childIdx]` to reflect\n   the new child sizes. (Parent's total size is unchanged since we just\n   moved entries between siblings.)\n\n**Redistribute from right sibling to deficient child (both inner nodes):**\n1. Pull down parent's separator `keys[childIdx]` — **append** it to deficient child's keys\n   (insert at the end, since the moved child goes to the end).\n2. Move right sibling's first child to the **end** of deficient child's children.\n   Move right sibling's first `sizes` entry to the end of deficient's sizes too.\n3. Push up right sibling's first key to replace parent's `keys[childIdx]`.\n4. Remove right sibling's first key, first child, and first size entry.\n5. Update parent's `sizes[childIdx]` and `sizes[childIdx+1]` to reflect\n   the new child sizes. (Parent's total size is unchanged.)\n\n**Merge two inner nodes:**\n1. Pull down the parent's separator between them into the left node's keys.\n2. Append all of right node's keys, children, and sizes to left node.\n3. Remove right child and its separator from parent.\n4. Update parent's `sizes` for the merged left child.\n\n### GetByIndex\n\nUse `sizes[]` at each inner node to find which child contains the i-th\nleaf entry, then descend. At the leaf, index directly into `keys[i]`/`values[i]`.\nPanics if index is out of range (matching avl behavior).\n\n### Stack-based iteration\n\nAll iteration uses a stack of `(innerNode, childIndex)` pairs representing\nthe path from root to the current leaf. When a leaf is exhausted, pop the\nstack, advance (or retreat) the child index, and descend to the next leaf.\nAmortized O(1) per entry — each node is pushed/popped at most once across\nthe full traversal.\n\nThe stack is a local slice built during iteration and discarded after —\nit creates no persistent references to nodes and does not affect ref-counts.\n\n### Iterate / ReverseIterate (key-range)\n\n**Ascending (Iterate):**\n1. Descend from root using separator keys to find the leaf containing `start`\n   (or descend to the leftmost leaf if start is \"\"), recording the path.\n2. Within the leaf, find the first key \u003e= start.\n3. Visit entries from that position forward.\n4. When the leaf is exhausted, advance to the next leaf via the stack:\n   pop the stack, increment childIdx. If childIdx is now past the last\n   child, pop again (repeat until a valid childIdx is found or the stack\n   is empty — if empty, iteration is done). Then descend to the leftmost\n   leaf of that child, pushing each inner node onto the stack.\n5. Stop when key \u003e= end (if end != \"\") or tree is exhausted.\n6. Call `cb(key, value)` for each entry; stop if cb returns true.\n\n**Descending (ReverseIterate):**\n1. Descend from root to find the leaf containing `end` (or the rightmost\n   leaf if end is \"\"), recording the path.\n2. Within the leaf, find the last key \u003c= end.\n3. Visit entries from that position backward.\n4. When the leaf is exhausted going backward, retreat to the previous leaf\n   via the stack: pop the stack, decrement childIdx. If childIdx \u003c 0, pop\n   again (repeat until a valid childIdx is found or the stack is empty —\n   if empty, iteration is done). Then descend to the rightmost leaf of\n   that child, pushing each inner node onto the stack.\n5. Stop when key \u003c start (if start != \"\") or tree is exhausted.\n6. Call `cb(key, value)` for each entry; stop if cb returns true.\n\n### IterateByOffset / ReverseIterateByOffset\n\n**Ascending:**\n1. Use `sizes[]` to descend to the leaf containing the offset-th entry,\n   recording the path. Maintain a running offset counter: at each inner node,\n   subtract `sizes[i]` for each skipped child. When `offset \u003c sizes[i]`,\n   descend into that child. Upon reaching a leaf, the remaining offset is\n   the position within the leaf.\n2. Visit entries from that position forward, advancing through leaves via\n   the stack (same as Iterate), counting up to `count`.\n\n**Descending:**\nThe descending view is: entries in reverse sorted order, 0-indexed from\nthe largest key. offset=0 is the largest, offset=1 is the second-largest, etc.\n\n1. Compute the ascending index of the starting entry:\n   `ascIdx = size - 1 - offset` (the entry at position `offset` in descending order).\n2. Use `sizes[]` to descend to the leaf containing entry `ascIdx`,\n   recording the path.\n3. Visit entries from that position backward, retreating through leaves via\n   the stack (same as ReverseIterate), counting up to `count`.\n4. If `ascIdx \u003c 0` or `offset \u003e= size` or `count \u003c= 0`, return false.\n\n## Split details\n\n### Leaf split\n\nGiven a leaf with `fanout+1` entries (one over capacity):\n\n**Detection:** after inserting the new key, if it ended up at position\n`fanout` in the overflowed `fanout+1`-entry leaf, it is an append-pattern\ninsert (the new key is greater than all pre-existing keys).\n\n**90/10 split (append pattern):**\n- `mid = fanout - 1`\n- Left leaf keeps entries `[0, fanout-1)` = `fanout-1` entries.\n- New right leaf gets entries `[fanout-1, fanout+1)` = 2 entries.\n- Left has `fanout-1` entries (~97% full), right has 2.\n- For large fanouts, the right leaf may be below the `fanout/2` deletion\n  threshold. This is intentional — the 90/10 split prioritizes fill factor\n  for append-heavy workloads. If a subsequent Remove causes the right leaf\n  to underflow, the standard rebalance logic handles it.\n\n**50/50 split (random pattern):**\n- `mid = (fanout + 1) / 2`\n- Left leaf keeps entries `[0, mid)`.\n- New right leaf gets entries `[mid, fanout+1)`.\n\nIn both cases:\n- Separator promoted to parent = `right.keys[0]`.\n- No linked list updates needed (no sibling pointers).\n\n### Inner split\n\nGiven an inner node with `fanout+1` children:\n- `mid = (fanout + 1) / 2`\n- Left keeps children `[0, mid)` with keys `[0, mid-1)` and sizes `[0, mid)`.\n- Right gets children `[mid, fanout+1)` with keys `[mid, fanout)` and sizes `[mid, fanout+1)`.\n- The separator at `keys[mid-1]` is **promoted** to the parent (not kept in either child).\n- Parent's sizes entry for the original child is replaced by the sum of left's sizes,\n  and a new entry is inserted for the right child with the sum of right's sizes.\n\n## Minimum fanout\n\nFanout must be \u003e= 4. With fanout 3, a leaf splits into (2, 2) and\nthe minimum occupancy is 1, which makes merge logic degenerate. Fanout 4\ngives minimum occupancy 2 and clean split/merge behavior.\n\n`NewBPTreeN` panics if fanout \u003c 4.\n\n## File structure\n\n```\nexamples/gno.land/p/nt/bptree/v0/\n  gnomod.toml\n  doc.gno          — package doc\n  node.gno         — leafNode, innerNode, node interface, binary search\n  tree.gno         — BPTree struct, constructors, ITree methods,\n                     insert/split, remove/merge/redistribute, iteration\n  tree_test.gno    — comprehensive tests (mirroring avl tests + B+ tree specifics)\n```\n\nTwo source files (`node.gno` + `tree.gno`) plus one test file. The node\ntypes and tree logic are tightly coupled, so fewer files is better than\nspreading thin.\n\n## Ref-count safety\n\nIn Gno's persistence model, objects with ref-count \u003e= 2 \"escape\" — they are\npersisted separately in an iavl tree rather than inlined in their parent's\nserialized form. Once escaped, they are forever escaped. This is expensive\nand should be avoided.\n\n**Design constraint: every node must have exactly one persistent reference.**\n\nThis means:\n- **No sibling pointers** on leaf nodes (a leaf would be referenced by both\n  its parent and its neighbor → ref-count \u003e= 2).\n- **No `first`/`last` pointers** on BPTree (a leaf would be referenced by\n  both BPTree and its parent inner node → ref-count \u003e= 2).\n- **No shared subtrees** (each child is owned by exactly one parent).\n\nThe tree structure is a pure tree (not a graph) — every node has exactly\none parent reference. The `BPTree.root` is the sole reference to the root\nnode. Each `innerNode.children[i]` is the sole reference to child `i`.\n\nIteration uses an ephemeral stack (local slice) that is built and discarded\nwithin a single method call. It does not create any persistent references.\n\n## Edge cases (must match avl behavior exactly)\n\n### Values and keys\n- `nil` is a valid value. `Set(\"foo\", nil)` stores it; `Get(\"foo\")` returns\n  `nil`; `Has(\"foo\")` returns `true`. `Remove(\"foo\")` returns `(nil, true)`.\n- `\"\"` is a valid key. Stored, retrieved, removed like any other key.\n- `Get` on missing key returns `nil` (use `Has` to distinguish from a stored\n  nil value).\n- `Remove` on missing key returns `(nil, false)`.\n- `Set` same key twice replaces value, returns `updated=true`.\n\n### Zero-value and structural\n- `var t BPTree` must work — zero-value tree is usable without a constructor.\n  All methods work on it immediately. This means `root == nil` must be handled\n  gracefully everywhere, and the default `fanout` (0) must be promoted to 32\n  on first use. `Set` promotes on first call: `if t.fanout == 0 { t.fanout = 32 }`.\n  All other methods that read `t.fanout` guard with `if t.root == nil` first, so\n  fanout is always initialized before it is read. Once set, fanout never resets\n  (even if tree becomes empty again).\n- Remove last key → tree returns to empty state (root = nil).\n- Insert after removing everything works normally.\n- `Size()` on empty tree returns 0.\n- `GetByIndex` on empty tree panics. Negative index or index \u003e= size also panics.\n- Single entry: root is a leafNode with 1 entry.\n- Root is a leaf: no inner nodes until first split.\n- Root inner node collapses: after merge leaves root with 1 child,\n  replace root with that child.\n\n### Separator key maintenance\n- When the minimum key of a subtree changes (deletion of leftmost key,\n  or redistribution), the parent's separator key must be updated.\n  After modifying a child, check if `keys[childIdx-1]` still equals\n  `children[childIdx].minKey()`.\n\n### Iteration — key range\n- All iteration on an empty tree returns `false` without calling cb.\n- `Iterate(\"\", \"\", cb)` visits ALL entries ascending (canonical pattern).\n- `ReverseIterate(\"\", \"\", cb)` visits ALL entries descending.\n- `Iterate(\"a\", \"a\", cb)` → empty. [a,a) = nothing (start inclusive, end exclusive).\n- `ReverseIterate(\"a\", \"a\", cb)` → visits \"a\". [a,a] = one entry (both inclusive).\n- `Iterate(\"z\", \"a\", cb)` → empty (no validation, logic just excludes everything).\n- `ReverseIterate(\"z\", \"a\", cb)` → empty (bounds don't swap).\n- `Iterate(\"\", \"a\", cb)` → visits all keys \u003c \"a\".\n- `ReverseIterate(\"a\", \"\", cb)` → visits all keys \u003e= \"a\", in descending order.\n- Return value = true if callback stopped iteration early, false otherwise.\n\n### Iteration — offset\n- `IterateByOffset(0, 0, cb)` → nothing (count \u003c= 0).\n- `IterateByOffset(size, 1, cb)` → nothing (offset \u003e= size).\n- `ReverseIterateByOffset(0, N, cb)` → starts at largest key, takes N descending.\n- `ReverseIterateByOffset(1, 2, cb)` on [a,b,c,d,e] → [d, c].\n- Negative count → treated as count \u003c= 0 (no iteration).\n- Negative offset → clamped to 0 (avl silently treats negative as 0; we do the same explicitly).\n"},{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `bptree` - Mutable B+ tree\n\nA mutable, in-place B+ tree for storing key-value data in Gno realms. Exposes the same `ITree` interface as `gno.land/p/nt/avl/v0` but uses a B+ tree internally — fewer pointer dereferences per operation and better cache locality, with a configurable fanout.\n\n## Usage\n\n```go\npackage myrealm\n\nimport \"gno.land/p/nt/bptree/v0\"\n\n// Zero value is usable (fanout 32). Persisted across transactions.\nvar tree bptree.BPTree\n\nfunc Set(key string, value int) {\n    tree.Set(key, value)\n}\n\nfunc Get(key string) int {\n    raw := tree.Get(key)\n    if raw == nil {\n        panic(\"not found\")\n    }\n    return raw.(int)\n}\n\nfunc RangeAsc(start, end string) {\n    tree.Iterate(start, end, func(key string, value any) bool {\n        // return true to stop early\n        return false\n    })\n}\n```\n\nFor a different fanout, use a constructor:\n\n```go\ntree := bptree.NewBPTreeN(64) // fanout 64\n```\n\n## API\n\n```go\ntype BPTree struct{ /* unexported */ }\n\nfunc NewBPTree32() *BPTree            // fanout 32\nfunc NewBPTreeN(fanout int) *BPTree   // panics if fanout \u003c 4\n\n// Read\nfunc (t *BPTree) Size() int\nfunc (t *BPTree) Has(key string) bool\nfunc (t *BPTree) Get(key string) (value any) // nil if the key is absent\nfunc (t *BPTree) GetByIndex(index int) (key string, value any)\nfunc (t *BPTree) Iterate(start, end string, cb IterCbFn) bool\nfunc (t *BPTree) ReverseIterate(start, end string, cb IterCbFn) bool\nfunc (t *BPTree) IterateByOffset(offset, count int, cb IterCbFn) bool\nfunc (t *BPTree) ReverseIterateByOffset(offset, count int, cb IterCbFn) bool\n\n// Write\nfunc (t *BPTree) Set(key string, value any) (updated bool)\nfunc (t *BPTree) Remove(key string) (value any, removed bool)\n\ntype IterCbFn func(key string, value any) bool\n\ntype ITree interface { /* same shape as BPTree's methods */ }\n```\n\nThe zero value of `BPTree` is a usable empty tree (fanout 32). `Iterate` uses `[start, end)` (start inclusive, end exclusive); `ReverseIterate` uses `[start, end]` (both inclusive). Empty strings mean unbounded. Callbacks return `true` to stop early. `GetByIndex` panics on out-of-range indices.\n\nThe tree must not be modified during iteration (no `Set` or `Remove` from the callback).\n\n## Subpackages\n\n- `gno.land/p/nt/bptree/v0/list` - ordered list built on top of `BPTree`.\n- `gno.land/p/nt/bptree/v0/pager` - pagination helper for trees and lists.\n- `gno.land/p/nt/bptree/v0/rotree` - read-only view of a `BPTree`.\n\n## Notes\n\n- API and semantics match `gno.land/p/nt/avl/v0` exactly — `\"\"` is a valid key, `Get` returns `nil` for a missing key (use `Has` to distinguish a stored `nil`), and `Remove` returns `(nil, false)`.\n- Never return the live `*BPTree` from a realm getter: a caller can then call `Set`/`Remove` on it under your realm's authority. Return values, copies, or a read-only `rotree` view.\n- Sequential keys from `seqid` (`gno.land/p/nt/seqid/v0`) pair well here: monotonic inserts hit the append-optimized split path.\n- Fanout must be `\u003e= 4`. Higher fanouts mean shallower trees and fewer object loads per lookup, at the cost of larger individual node objects.\n- Each node (leaf or inner) is persisted as a separate object, so reads only load the `O(log n)` nodes on the search path — same storage-efficiency benefit as `avl`.\n- No sibling pointers or `first`/`last` shortcuts: iteration uses an ephemeral stack to keep every persisted node at ref-count 1 (avoids Gno's object-escape penalty).\n"},{"name":"doc.gno","body":"// Package bptree provides a mutable B+ tree implementation for storing\n// key-value data in Gno realms. It implements the same ITree interface\n// as the avl package but uses a B+ tree internally for better cache\n// locality and fewer pointer dereferences per operation.\n//\n// The fanout (maximum number of children per inner node, and maximum\n// number of entries per leaf node) is configurable:\n//\n//\ttree := bptree.NewBPTree32()    // fanout 32\n//\ttree := bptree.NewBPTreeN(64)   // fanout 64\n//\n// The zero value is usable as an empty tree with fanout 32:\n//\n//\tvar tree bptree.BPTree\n//\ttree.Set(\"key\", \"value\")\npackage bptree\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/v0\"\ngno = \"0.9\"\n"},{"name":"node.gno","body":"package bptree\n\n// node is the interface satisfied by both inner and leaf nodes.\ntype node interface {\n\tisLeaf() bool\n\tnodeSize() int // total leaf entries in subtree\n\tminKey() string\n}\n\n//----------------------------------------\n// leafNode\n\ntype leafNode struct {\n\tkeys   []string\n\tvalues []*any // each value is a separate object for lazy loading\n}\n\nfunc newLeafNode(fanout int) *leafNode {\n\treturn \u0026leafNode{\n\t\tkeys:   make([]string, 0, fanout),\n\t\tvalues: make([]*any, 0, fanout),\n\t}\n}\n\nfunc (n *leafNode) isLeaf() bool   { return true }\nfunc (n *leafNode) nodeSize() int  { return len(n.keys) }\nfunc (n *leafNode) minKey() string { return n.keys[0] }\n\n// find returns the index where key is or would be inserted,\n// and whether an exact match was found.\nfunc (n *leafNode) find(key string) (int, bool) {\n\tlo, hi := 0, len(n.keys)\n\tfor lo \u003c hi {\n\t\tmid := lo + (hi-lo)/2\n\t\tif n.keys[mid] \u003c key {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\tif lo \u003c len(n.keys) \u0026\u0026 n.keys[lo] == key {\n\t\treturn lo, true\n\t}\n\treturn lo, false\n}\n\n// insertAt inserts a key-value pair at the given position.\nfunc (n *leafNode) insertAt(pos int, key string, value *any) {\n\tn.keys = append(n.keys, \"\")\n\tcopy(n.keys[pos+1:], n.keys[pos:])\n\tn.keys[pos] = key\n\n\tn.values = append(n.values, nil)\n\tcopy(n.values[pos+1:], n.values[pos:])\n\tn.values[pos] = value\n}\n\n// removeAt removes the entry at the given position and returns the removed key and value.\nfunc (n *leafNode) removeAt(pos int) (string, *any) {\n\tkey := n.keys[pos]\n\tvalue := n.values[pos]\n\n\tcopy(n.keys[pos:], n.keys[pos+1:])\n\tn.keys[len(n.keys)-1] = \"\"\n\tn.keys = n.keys[:len(n.keys)-1]\n\n\tcopy(n.values[pos:], n.values[pos+1:])\n\tn.values[len(n.values)-1] = nil\n\tn.values = n.values[:len(n.values)-1]\n\n\treturn key, value\n}\n\n//----------------------------------------\n// innerNode\n\ntype innerNode struct {\n\tkeys     []string // separator keys; keys[i] = minKey of children[i+1]\n\tchildren []node\n\tsizes    []int // sizes[i] = total leaf entries in children[i]\n}\n\nfunc newInnerNode(fanout int) *innerNode {\n\treturn \u0026innerNode{\n\t\tkeys:     make([]string, 0, fanout-1),\n\t\tchildren: make([]node, 0, fanout),\n\t\tsizes:    make([]int, 0, fanout),\n\t}\n}\n\nfunc (n *innerNode) isLeaf() bool { return false }\nfunc (n *innerNode) nodeSize() int {\n\tsum := 0\n\tfor _, s := range n.sizes {\n\t\tsum += s\n\t}\n\treturn sum\n}\nfunc (n *innerNode) minKey() string { return n.children[0].minKey() }\n\n// findChild returns the child index for the given key.\nfunc (n *innerNode) findChild(key string) int {\n\t// Binary search: find the rightmost i where keys[i] \u003c= key.\n\tlo, hi := 0, len(n.keys)\n\tfor lo \u003c hi {\n\t\tmid := lo + (hi-lo)/2\n\t\tif n.keys[mid] \u003c= key {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\n// insertChildAt inserts a new child with its separator key at the given position.\n// The separator key is placed at keys[pos-1] (since keys[i] = minKey of children[i+1]).\nfunc (n *innerNode) insertChildAt(pos int, sep string, child node, sz int) {\n\t// Insert separator at keys[pos-1].\n\tkeyPos := pos - 1\n\tn.keys = append(n.keys, \"\")\n\tcopy(n.keys[keyPos+1:], n.keys[keyPos:])\n\tn.keys[keyPos] = sep\n\n\t// Insert child at children[pos].\n\tn.children = append(n.children, nil)\n\tcopy(n.children[pos+1:], n.children[pos:])\n\tn.children[pos] = child\n\n\t// Insert size at sizes[pos].\n\tn.sizes = append(n.sizes, 0)\n\tcopy(n.sizes[pos+1:], n.sizes[pos:])\n\tn.sizes[pos] = sz\n}\n\n// removeChildAt removes the child at pos and its associated separator key.\nfunc (n *innerNode) removeChildAt(pos int) {\n\t// Determine which separator to remove.\n\t// keys[i] separates children[i] from children[i+1].\n\t// Removing children[pos]: if pos \u003e 0, remove keys[pos-1]; else remove keys[0].\n\tkeyPos := pos\n\tif pos \u003e 0 {\n\t\tkeyPos = pos - 1\n\t}\n\tif len(n.keys) \u003e 0 {\n\t\tcopy(n.keys[keyPos:], n.keys[keyPos+1:])\n\t\tn.keys[len(n.keys)-1] = \"\"\n\t\tn.keys = n.keys[:len(n.keys)-1]\n\t}\n\n\tcopy(n.children[pos:], n.children[pos+1:])\n\tn.children[len(n.children)-1] = nil\n\tn.children = n.children[:len(n.children)-1]\n\n\tcopy(n.sizes[pos:], n.sizes[pos+1:])\n\tn.sizes = n.sizes[:len(n.sizes)-1]\n}\n"},{"name":"tree.gno","body":"package bptree\n\ntype ITree interface {\n\tSize() int\n\tHas(key string) bool\n\tGet(key string) any\n\tGetByIndex(index int) (key string, value any)\n\tIterate(start, end string, cb IterCbFn) bool\n\tReverseIterate(start, end string, cb IterCbFn) bool\n\tIterateByOffset(offset int, count int, cb IterCbFn) bool\n\tReverseIterateByOffset(offset int, count int, cb IterCbFn) bool\n\tSet(key string, value any) (updated bool)\n\tRemove(key string) (value any, removed bool)\n}\n\ntype IterCbFn func(key string, value any) bool\n\n// Verify BPTree implements ITree.\nvar _ ITree = (*BPTree)(nil)\n\n// The zero value is usable as an empty tree with fanout 32.\ntype BPTree struct {\n\troot   node\n\tsize   int\n\tfanout int\n}\n\n// NewBPTreeN creates a new empty B+ tree with the given fanout.\n// It panics when fanout is lower than 4.\nfunc NewBPTreeN(fanout int) *BPTree {\n\tif fanout \u003c 4 {\n\t\tpanic(\"bptree: fanout must be \u003e= 4\")\n\t}\n\treturn \u0026BPTree{fanout: fanout}\n}\n\n// NewBPTree32 creates a new empty B+ tree with fanout 32.\nfunc NewBPTree32() *BPTree {\n\treturn NewBPTreeN(32)\n}\n\nfunc (t *BPTree) Size() int {\n\treturn t.size\n}\n\nfunc (t *BPTree) Has(key string) bool {\n\tif t.root == nil {\n\t\treturn false\n\t}\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\t_, found := leaf.find(key)\n\treturn found\n}\n\n// Get retrieves the value associated with the given key.\n// It returns the value if the key exists, or nil if it doesn't.\n// This allows for a simpler usage pattern with type assertions:\n//\n//\tif value, ok := tree.Get(\"key\").(MyType); ok {\n//\t    // use value\n//\t}\n//\n// Use Has to distinguish a stored nil value from a missing key.\nfunc (t *BPTree) Get(key string) any {\n\tif t.root == nil {\n\t\treturn nil\n\t}\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\tpos, found := leaf.find(key)\n\tif !found {\n\t\treturn nil\n\t}\n\treturn *leaf.values[pos]\n}\n\n// GetByIndex returns the key-value pair at the given 0-based index.\n// Panics if index is out of range.\nfunc (t *BPTree) GetByIndex(index int) (key string, value any) {\n\tif t.root == nil || index \u003c 0 || index \u003e= t.size {\n\t\tpanic(\"GetByIndex asked for invalid index\")\n\t}\n\tn := t.root\n\trem := index\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tfound := false\n\t\tfor i, s := range inner.sizes {\n\t\t\tif rem \u003c s {\n\t\t\t\tn = inner.children[i]\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trem -= s\n\t\t}\n\t\tif !found {\n\t\t\tpanic(\"GetByIndex asked for invalid index\")\n\t\t}\n\t}\n\tleaf := n.(*leafNode)\n\treturn leaf.keys[rem], *leaf.values[rem]\n}\n\n//----------------------------------------\n// Set\n\n// pathEntry records a step in the root-to-leaf descent.\ntype pathEntry struct {\n\tinner    *innerNode\n\tchildIdx int\n}\n\n// Set inserts or updates a key-value pair. Returns true if the key already existed.\nfunc (t *BPTree) Set(key string, value any) (updated bool) {\n\tif t.fanout == 0 {\n\t\tt.fanout = 32\n\t}\n\tfanout := t.fanout\n\n\tvp := \u0026value // wrap value in *any for lazy loading\n\n\t// Empty tree: create a single leaf.\n\tif t.root == nil {\n\t\tleaf := newLeafNode(fanout)\n\t\tleaf.keys = append(leaf.keys, key)\n\t\tleaf.values = append(leaf.values, vp)\n\t\tt.root = leaf\n\t\tt.size = 1\n\t\treturn false\n\t}\n\n\t// Descend to the leaf, recording the path.\n\tvar path []pathEntry\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tpath = append(path, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\n\t// Check if key already exists.\n\tpos, found := leaf.find(key)\n\tif found {\n\t\t*leaf.values[pos] = value\n\t\treturn true\n\t}\n\n\t// Insert new key-value.\n\tleaf.insertAt(pos, key, vp)\n\tt.size++\n\n\t// Increment sizes up the path.\n\tfor i := range path {\n\t\tpath[i].inner.sizes[path[i].childIdx]++\n\t}\n\n\t// Split if leaf overflows.\n\tif len(leaf.keys) \u003e fanout {\n\t\tt.splitLeaf(leaf, pos, path)\n\t}\n\n\treturn false\n}\n\n// splitLeaf splits an overflowed leaf node. Uses 90/10 split for append\n// patterns (insertPos == fanout) and 50/50 otherwise.\nfunc (t *BPTree) splitLeaf(leaf *leafNode, insertPos int, path []pathEntry) {\n\tfanout := t.fanout\n\n\t// 90/10 split for append pattern: new key ended up at position fanout\n\t// (greater than all pre-existing keys).\n\tvar mid int\n\tif insertPos == fanout {\n\t\tmid = fanout - 1\n\t} else {\n\t\tmid = (fanout + 1) / 2\n\t}\n\n\t// Create right leaf with entries [mid, fanout+1).\n\tright := newLeafNode(fanout)\n\tright.keys = append(right.keys, leaf.keys[mid:]...)\n\tright.values = append(right.values, leaf.values[mid:]...)\n\n\t// Truncate left leaf to [0, mid).\n\tfor i := mid; i \u003c len(leaf.keys); i++ {\n\t\tleaf.keys[i] = \"\"\n\t\tleaf.values[i] = nil\n\t}\n\tleaf.keys = leaf.keys[:mid]\n\tleaf.values = leaf.values[:mid]\n\n\t// Promote separator to parent.\n\tsep := right.keys[0]\n\tleftSize := len(leaf.keys)\n\trightSize := len(right.keys)\n\n\tif len(path) == 0 {\n\t\t// Root was the leaf; create a new inner root.\n\t\tnewRoot := newInnerNode(fanout)\n\t\tnewRoot.keys = append(newRoot.keys, sep)\n\t\tnewRoot.children = append(newRoot.children, leaf, right)\n\t\tnewRoot.sizes = append(newRoot.sizes, leftSize, rightSize)\n\t\tt.root = newRoot\n\t\treturn\n\t}\n\n\t// Insert into parent.\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\n\tparent.sizes[childIdx] = leftSize\n\tparent.insertChildAt(childIdx+1, sep, right, rightSize)\n\n\tif len(parent.children) \u003e fanout {\n\t\tt.splitInner(parent, path[:len(path)-1])\n\t}\n}\n\n// splitInner splits an overflowed inner node (always 50/50).\nfunc (t *BPTree) splitInner(inner *innerNode, path []pathEntry) {\n\tfanout := t.fanout\n\tmid := (fanout + 1) / 2\n\n\tpromotedKey := inner.keys[mid-1]\n\n\tright := newInnerNode(fanout)\n\tright.keys = append(right.keys, inner.keys[mid:]...)\n\tright.children = append(right.children, inner.children[mid:]...)\n\tright.sizes = append(right.sizes, inner.sizes[mid:]...)\n\n\tfor i := mid - 1; i \u003c len(inner.keys); i++ {\n\t\tinner.keys[i] = \"\"\n\t}\n\tfor i := mid; i \u003c len(inner.children); i++ {\n\t\tinner.children[i] = nil\n\t}\n\tinner.keys = inner.keys[:mid-1]\n\tinner.children = inner.children[:mid]\n\tinner.sizes = inner.sizes[:mid]\n\n\tif len(path) == 0 {\n\t\tnewRoot := newInnerNode(fanout)\n\t\tnewRoot.keys = append(newRoot.keys, promotedKey)\n\t\tnewRoot.children = append(newRoot.children, inner, right)\n\t\tnewRoot.sizes = append(newRoot.sizes, inner.nodeSize(), right.nodeSize())\n\t\tt.root = newRoot\n\t\treturn\n\t}\n\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\n\tparent.sizes[childIdx] = inner.nodeSize()\n\tparent.insertChildAt(childIdx+1, promotedKey, right, right.nodeSize())\n\n\tif len(parent.children) \u003e fanout {\n\t\tt.splitInner(parent, path[:len(path)-1])\n\t}\n}\n\n//----------------------------------------\n// Remove\n\n// Remove deletes a key. Returns the old value and true if the key was found.\nfunc (t *BPTree) Remove(key string) (value any, removed bool) {\n\tif t.root == nil {\n\t\treturn nil, false\n\t}\n\tfanout := t.fanout\n\n\tvar path []pathEntry\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tpath = append(path, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\n\tpos, found := leaf.find(key)\n\tif !found {\n\t\treturn nil, false\n\t}\n\n\tvar vp *any\n\t_, vp = leaf.removeAt(pos)\n\tvalue = *vp\n\tt.size--\n\n\tfor i := range path {\n\t\tpath[i].inner.sizes[path[i].childIdx]--\n\t}\n\n\t// Handle root leaf.\n\tif len(path) == 0 {\n\t\tif len(leaf.keys) == 0 {\n\t\t\tt.root = nil\n\t\t}\n\t\treturn value, true\n\t}\n\n\t// Check underflow.\n\tminKeys := fanout / 2\n\tif len(leaf.keys) \u003e= minKeys {\n\t\tif pos == 0 {\n\t\t\tt.updateSeparator(path)\n\t\t}\n\t\treturn value, true\n\t}\n\n\t// If the minimum key was removed, fix ancestor separators before rebalancing.\n\tif pos == 0 {\n\t\tt.updateSeparator(path)\n\t}\n\n\t// Rebalance.\n\tt.rebalanceLeaf(leaf, path)\n\treturn value, true\n}\n\n// updateSeparator fixes the parent's separator key if the child's min key changed\n// (e.g., after removing the leftmost entry).\nfunc (t *BPTree) updateSeparator(path []pathEntry) {\n\tfor i := len(path) - 1; i \u003e= 0; i-- {\n\t\tpe := path[i]\n\t\tif pe.childIdx \u003e 0 {\n\t\t\tchild := pe.inner.children[pe.childIdx]\n\t\t\tpe.inner.keys[pe.childIdx-1] = child.minKey()\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// rebalanceLeaf handles a leaf that has underflowed (\u003c fanout/2 entries).\n// Tries redistribute from left, then right, then merges with a sibling.\nfunc (t *BPTree) rebalanceLeaf(leaf *leafNode, path []pathEntry) {\n\tfanout := t.fanout\n\tminKeys := fanout / 2\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\n\t// Try redistribute from left sibling.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*leafNode)\n\t\tif len(leftSib.keys) \u003e minKeys {\n\t\t\tk, v := leftSib.removeAt(len(leftSib.keys) - 1)\n\t\t\tleaf.insertAt(0, k, v)\n\t\t\tparent.keys[childIdx-1] = leaf.keys[0]\n\t\t\tparent.sizes[childIdx-1] = len(leftSib.keys)\n\t\t\tparent.sizes[childIdx] = len(leaf.keys)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Try redistribute from right sibling.\n\tif childIdx \u003c len(parent.children)-1 {\n\t\trightSib := parent.children[childIdx+1].(*leafNode)\n\t\tif len(rightSib.keys) \u003e minKeys {\n\t\t\tk, v := rightSib.removeAt(0)\n\t\t\tleaf.insertAt(len(leaf.keys), k, v)\n\t\t\tparent.keys[childIdx] = rightSib.keys[0]\n\t\t\tparent.sizes[childIdx] = len(leaf.keys)\n\t\t\tparent.sizes[childIdx+1] = len(rightSib.keys)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Merge.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*leafNode)\n\t\tmergeLeaves(leftSib, leaf, parent, childIdx)\n\t} else {\n\t\trightSib := parent.children[childIdx+1].(*leafNode)\n\t\tmergeLeaves(leaf, rightSib, parent, childIdx+1)\n\t}\n\n\tt.rebalanceInner(path[:len(path)-1])\n}\n\n// mergeLeaves merges right leaf into left and removes right from parent.\nfunc mergeLeaves(left, right *leafNode, parent *innerNode, rightIdx int) {\n\tleft.keys = append(left.keys, right.keys...)\n\tleft.values = append(left.values, right.values...)\n\tparent.removeChildAt(rightIdx)\n\tparent.sizes[rightIdx-1] = len(left.keys)\n}\n\n// rebalanceInner handles an inner node that has underflowed after a child merge.\nfunc (t *BPTree) rebalanceInner(path []pathEntry) {\n\tif len(path) == 0 {\n\t\troot := t.root.(*innerNode)\n\t\tif len(root.children) == 1 {\n\t\t\tt.root = root.children[0]\n\t\t}\n\t\treturn\n\t}\n\n\tpe := path[len(path)-1]\n\tparent := pe.inner\n\tchildIdx := pe.childIdx\n\tchild := parent.children[childIdx].(*innerNode)\n\tfanout := t.fanout\n\tminChildren := fanout / 2\n\n\tif len(child.children) \u003e= minChildren {\n\t\tif childIdx \u003e 0 {\n\t\t\tparent.keys[childIdx-1] = child.minKey()\n\t\t}\n\t\treturn\n\t}\n\n\t// Try redistribute from left sibling.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*innerNode)\n\t\tif len(leftSib.children) \u003e minChildren {\n\t\t\tredistributeInnerLeft(parent, childIdx, leftSib, child)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Try redistribute from right sibling.\n\tif childIdx \u003c len(parent.children)-1 {\n\t\trightSib := parent.children[childIdx+1].(*innerNode)\n\t\tif len(rightSib.children) \u003e minChildren {\n\t\t\tredistributeInnerRight(parent, childIdx, child, rightSib)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Merge.\n\tif childIdx \u003e 0 {\n\t\tleftSib := parent.children[childIdx-1].(*innerNode)\n\t\tmergeInner(leftSib, child, parent, childIdx)\n\t} else {\n\t\trightSib := parent.children[childIdx+1].(*innerNode)\n\t\tmergeInner(child, rightSib, parent, childIdx+1)\n\t}\n\n\tgrandPath := path[:len(path)-1]\n\tif len(grandPath) == 0 {\n\t\troot := t.root.(*innerNode)\n\t\tif len(root.children) == 1 {\n\t\t\tt.root = root.children[0]\n\t\t}\n\t\treturn\n\t}\n\n\tgpe := grandPath[len(grandPath)-1]\n\tgparent := gpe.inner\n\tgchildIdx := gpe.childIdx\n\tgchild := gparent.children[gchildIdx].(*innerNode)\n\tminChildren = t.fanout / 2\n\n\tif len(gchild.children) \u003c minChildren {\n\t\tt.rebalanceInner(grandPath)\n\t} else if gchildIdx \u003e 0 {\n\t\tgparent.keys[gchildIdx-1] = gchild.minKey()\n\t}\n}\n\n// redistributeInnerLeft moves the last child from left sibling to the\n// deficient child, pulling down the parent separator and pushing up a new one.\nfunc redistributeInnerLeft(parent *innerNode, childIdx int, leftSib, child *innerNode) {\n\tsep := parent.keys[childIdx-1]\n\tchild.keys = append(child.keys, \"\")\n\tcopy(child.keys[1:], child.keys)\n\tchild.keys[0] = sep\n\n\tlastChild := leftSib.children[len(leftSib.children)-1]\n\tlastSize := leftSib.sizes[len(leftSib.sizes)-1]\n\tchild.children = append(child.children, nil)\n\tcopy(child.children[1:], child.children)\n\tchild.children[0] = lastChild\n\tchild.sizes = append(child.sizes, 0)\n\tcopy(child.sizes[1:], child.sizes)\n\tchild.sizes[0] = lastSize\n\n\tparent.keys[childIdx-1] = leftSib.keys[len(leftSib.keys)-1]\n\n\tleftSib.keys[len(leftSib.keys)-1] = \"\"\n\tleftSib.keys = leftSib.keys[:len(leftSib.keys)-1]\n\tleftSib.children[len(leftSib.children)-1] = nil\n\tleftSib.children = leftSib.children[:len(leftSib.children)-1]\n\tleftSib.sizes = leftSib.sizes[:len(leftSib.sizes)-1]\n\n\tparent.sizes[childIdx-1] = leftSib.nodeSize()\n\tparent.sizes[childIdx] = child.nodeSize()\n}\n\n// redistributeInnerRight moves the first child from right sibling to the\n// deficient child, pulling down the parent separator and pushing up a new one.\nfunc redistributeInnerRight(parent *innerNode, childIdx int, child, rightSib *innerNode) {\n\tsep := parent.keys[childIdx]\n\tchild.keys = append(child.keys, sep)\n\n\tfirstChild := rightSib.children[0]\n\tfirstSize := rightSib.sizes[0]\n\tchild.children = append(child.children, firstChild)\n\tchild.sizes = append(child.sizes, firstSize)\n\n\tparent.keys[childIdx] = rightSib.keys[0]\n\n\tcopy(rightSib.keys, rightSib.keys[1:])\n\trightSib.keys[len(rightSib.keys)-1] = \"\"\n\trightSib.keys = rightSib.keys[:len(rightSib.keys)-1]\n\tcopy(rightSib.children, rightSib.children[1:])\n\trightSib.children[len(rightSib.children)-1] = nil\n\trightSib.children = rightSib.children[:len(rightSib.children)-1]\n\tcopy(rightSib.sizes, rightSib.sizes[1:])\n\trightSib.sizes = rightSib.sizes[:len(rightSib.sizes)-1]\n\n\tparent.sizes[childIdx] = child.nodeSize()\n\tparent.sizes[childIdx+1] = rightSib.nodeSize()\n}\n\n// mergeInner merges right inner node into left, pulling down the parent\n// separator, and removes right from parent.\nfunc mergeInner(left, right *innerNode, parent *innerNode, rightIdx int) {\n\tsep := parent.keys[rightIdx-1]\n\tleft.keys = append(left.keys, sep)\n\tleft.keys = append(left.keys, right.keys...)\n\tleft.children = append(left.children, right.children...)\n\tleft.sizes = append(left.sizes, right.sizes...)\n\tparent.removeChildAt(rightIdx)\n\tparent.sizes[rightIdx-1] = left.nodeSize()\n}\n\n//----------------------------------------\n// Stack-based iteration\n\n// iterStack is a stack of (innerNode, childIdx) for traversal.\n// It is ephemeral — built during iteration, discarded after.\ntype iterStack []pathEntry\n\n// descendLeft descends to the leftmost leaf, pushing inner nodes onto the stack.\nfunc descendLeft(n node, stack *iterStack) *leafNode {\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\t*stack = append(*stack, pathEntry{inner, 0})\n\t\tn = inner.children[0]\n\t}\n\treturn n.(*leafNode)\n}\n\n// descendRight descends to the rightmost leaf, pushing inner nodes onto the stack.\nfunc descendRight(n node, stack *iterStack) *leafNode {\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tlast := len(inner.children) - 1\n\t\t*stack = append(*stack, pathEntry{inner, last})\n\t\tn = inner.children[last]\n\t}\n\treturn n.(*leafNode)\n}\n\n// advanceLeaf moves to the next leaf in ascending order via the stack.\n// Returns nil if there is no next leaf.\nfunc advanceLeaf(stack *iterStack) *leafNode {\n\tfor len(*stack) \u003e 0 {\n\t\ttop := \u0026(*stack)[len(*stack)-1]\n\t\ttop.childIdx++\n\t\tif top.childIdx \u003c len(top.inner.children) {\n\t\t\tn := top.inner.children[top.childIdx]\n\t\t\treturn descendLeft(n, stack)\n\t\t}\n\t\t*stack = (*stack)[:len(*stack)-1]\n\t}\n\treturn nil\n}\n\n// retreatLeaf moves to the previous leaf in descending order via the stack.\n// Returns nil if there is no previous leaf.\nfunc retreatLeaf(stack *iterStack) *leafNode {\n\tfor len(*stack) \u003e 0 {\n\t\ttop := \u0026(*stack)[len(*stack)-1]\n\t\ttop.childIdx--\n\t\tif top.childIdx \u003e= 0 {\n\t\t\tn := top.inner.children[top.childIdx]\n\t\t\treturn descendRight(n, stack)\n\t\t}\n\t\t*stack = (*stack)[:len(*stack)-1]\n\t}\n\treturn nil\n}\n\n//----------------------------------------\n// Iterate / ReverseIterate\n\n// Iterate calls cb for each key-value pair in [start, end) ascending order.\n// Empty start/end means no bound. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) Iterate(start, end string, cb IterCbFn) bool {\n\tif t.root == nil {\n\t\treturn false\n\t}\n\n\tvar stack iterStack\n\tvar leaf *leafNode\n\tvar pos int\n\n\tif start == \"\" {\n\t\tleaf = descendLeft(t.root, \u0026stack)\n\t\tpos = 0\n\t} else {\n\t\tleaf, pos, stack = t.descendToGE(start)\n\t\tif leaf == nil {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tfor leaf != nil {\n\t\tfor pos \u003c len(leaf.keys) {\n\t\t\tk := leaf.keys[pos]\n\t\t\tif end != \"\" \u0026\u0026 k \u003e= end {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif cb(k, *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tpos++\n\t\t}\n\t\tleaf = advanceLeaf(\u0026stack)\n\t\tpos = 0\n\t}\n\treturn false\n}\n\n// ReverseIterate calls cb for each key-value pair in [start, end] descending order.\n// Empty start/end means no bound. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) ReverseIterate(start, end string, cb IterCbFn) bool {\n\tif t.root == nil {\n\t\treturn false\n\t}\n\n\tvar stack iterStack\n\tvar leaf *leafNode\n\tvar pos int\n\n\tif end == \"\" {\n\t\tleaf = descendRight(t.root, \u0026stack)\n\t\tpos = len(leaf.keys) - 1\n\t} else {\n\t\tleaf, pos, stack = t.descendToLE(end)\n\t\tif leaf == nil {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tfor leaf != nil {\n\t\tfor pos \u003e= 0 {\n\t\t\tk := leaf.keys[pos]\n\t\t\tif start != \"\" \u0026\u0026 k \u003c start {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif cb(k, *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tpos--\n\t\t}\n\t\tleaf = retreatLeaf(\u0026stack)\n\t\tif leaf != nil {\n\t\t\tpos = len(leaf.keys) - 1\n\t\t}\n\t}\n\treturn false\n}\n\n// IterateByOffset calls cb for count entries starting at the offset-th entry\n// in ascending order. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) IterateByOffset(offset int, count int, cb IterCbFn) bool {\n\tif t.root == nil || offset \u003e= t.size || count \u003c= 0 {\n\t\treturn false\n\t}\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\n\tleaf, pos, stack := t.descendToOffset(offset)\n\n\tvisited := 0\n\tfor leaf != nil \u0026\u0026 visited \u003c count {\n\t\tfor pos \u003c len(leaf.keys) \u0026\u0026 visited \u003c count {\n\t\t\tif cb(leaf.keys[pos], *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tvisited++\n\t\t\tpos++\n\t\t}\n\t\tleaf = advanceLeaf(\u0026stack)\n\t\tpos = 0\n\t}\n\treturn false\n}\n\n// ReverseIterateByOffset calls cb for count entries starting at the offset-th\n// entry from the end, in descending order. Returns true if stopped early by cb.\n// The tree must not be modified during iteration (no Set or Remove from the callback).\nfunc (t *BPTree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool {\n\tif t.root == nil || offset \u003e= t.size || count \u003c= 0 {\n\t\treturn false\n\t}\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\n\tascIdx := t.size - 1 - offset\n\tleaf, pos, stack := t.descendToOffset(ascIdx)\n\n\tvisited := 0\n\tfor leaf != nil \u0026\u0026 visited \u003c count {\n\t\tfor pos \u003e= 0 \u0026\u0026 visited \u003c count {\n\t\t\tif cb(leaf.keys[pos], *leaf.values[pos]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tvisited++\n\t\t\tpos--\n\t\t}\n\t\tleaf = retreatLeaf(\u0026stack)\n\t\tif leaf != nil {\n\t\t\tpos = len(leaf.keys) - 1\n\t\t}\n\t}\n\treturn false\n}\n\n//----------------------------------------\n// Descent helpers for iteration\n\n// descendToGE descends to the first key \u003e= key, returning the leaf, position, and stack.\nfunc (t *BPTree) descendToGE(key string) (*leafNode, int, iterStack) {\n\tvar stack iterStack\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tstack = append(stack, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\tpos, _ := leaf.find(key)\n\tif pos \u003e= len(leaf.keys) {\n\t\tnext := advanceLeaf(\u0026stack)\n\t\tif next == nil {\n\t\t\treturn nil, 0, nil\n\t\t}\n\t\treturn next, 0, stack\n\t}\n\treturn leaf, pos, stack\n}\n\n// descendToLE descends to the last key \u003c= key, returning the leaf, position, and stack.\nfunc (t *BPTree) descendToLE(key string) (*leafNode, int, iterStack) {\n\tvar stack iterStack\n\tn := t.root\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tidx := inner.findChild(key)\n\t\tstack = append(stack, pathEntry{inner, idx})\n\t\tn = inner.children[idx]\n\t}\n\tleaf := n.(*leafNode)\n\tpos, found := leaf.find(key)\n\tif found {\n\t\treturn leaf, pos, stack\n\t}\n\tif pos \u003e 0 {\n\t\treturn leaf, pos - 1, stack\n\t}\n\tprev := retreatLeaf(\u0026stack)\n\tif prev == nil {\n\t\treturn nil, 0, nil\n\t}\n\treturn prev, len(prev.keys) - 1, stack\n}\n\n// descendToOffset descends to the offset-th entry using sizes[],\n// returning the leaf, position within leaf, and stack.\nfunc (t *BPTree) descendToOffset(offset int) (*leafNode, int, iterStack) {\n\tvar stack iterStack\n\tn := t.root\n\trem := offset\n\tfor !n.isLeaf() {\n\t\tinner := n.(*innerNode)\n\t\tfound := false\n\t\tfor i, s := range inner.sizes {\n\t\t\tif rem \u003c s {\n\t\t\t\tstack = append(stack, pathEntry{inner, i})\n\t\t\t\tn = inner.children[i]\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\trem -= s\n\t\t}\n\t\tif !found {\n\t\t\tpanic(\"descendToOffset: offset out of range\")\n\t\t}\n\t}\n\treturn n.(*leafNode), rem, stack\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ufmt","path":"gno.land/p/nt/ufmt/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `ufmt` - String formatting\n\nGno port of a subset of Go's `fmt` package (micro-fmt). Provides `Printf`, `Sprintf`, `Errorf` and friends for formatting strings with verb-based templates.\n\n## Usage\n\n```go\nimport \"gno.land/p/nt/ufmt/v0\"\n\ns := ufmt.Sprintf(\"hello %s, you are %d years old\", \"alice\", 30)\n// \"hello alice, you are 30 years old\"\n\nerr := ufmt.Errorf(\"invalid id: %q\", input)\n\nvar buf bytes.Buffer\nufmt.Fprintf(\u0026buf, \"balance: %d\", amount)\n\nline := ufmt.Sprintln(\"token\", symbol, \"transferred\") // adds spaces + newline\n```\n\n## API\n\n```go\n// Format and return a string.\nfunc Sprint(a ...any) string\nfunc Sprintf(format string, a ...any) string\nfunc Sprintln(a ...any) string\n\n// Format and write to an io.Writer.\nfunc Fprint(w io.Writer, a ...any) (n int, err error)\nfunc Fprintf(w io.Writer, format string, a ...any) (n int, err error)\nfunc Fprintln(w io.Writer, a ...any) (n int, err error)\n\n// Format and print to standard output.\nfunc Print(a ...any) (n int, err error)\nfunc Printf(format string, a ...any) (n int, err error)\nfunc Println(a ...any) (n int, err error)\n\n// Format and append to a byte slice.\nfunc Append(b []byte, a ...any) []byte\nfunc Appendf(b []byte, format string, a ...any) []byte\nfunc Appendln(b []byte, a ...any) []byte\n\n// Format and return an error.\nfunc Errorf(format string, args ...any) error\n```\n\n## Supported verbs\n\n| Verb | Meaning                                                                |\n|------|------------------------------------------------------------------------|\n| `%s` | String. Uses `String()` or `Error()` if implemented.                   |\n| `%d` | Integer (signed and unsigned, all widths).                             |\n| `%c` | Unicode character from rune/int code point.                            |\n| `%t` | Boolean: `true` or `false`.                                            |\n| `%q` | Double-quoted, escaped string.                                         |\n| `%x` | Hexadecimal (uint8 only).                                              |\n| `%f` / `%F` | Decimal float; default precision 6.                             |\n| `%e` / `%E` | Scientific notation float; default precision 2.                 |\n| `%g` / `%G` | Float, compact representation.                                  |\n| `%T` | Type name of the argument (basic types only).                          |\n| `%v` | Default representation appropriate for the value's type.               |\n| `%%` | Literal `%`.                                                           |\n\nWidth (`%5s`) and precision (`%.2f`) are supported for the relevant verbs.\n\n## Notes\n\n- Verb/type mismatches produce `%!verb(type=value)` strings, matching Go's `fmt` behaviour.\n- Missing or extra arguments panic.\n- Not supported: `%b`, `%o`, `%U`, `%p`, `%+v`, `%#v`, argument indexing, flags like `-`, `+`, `#`, `0`.\n- `Print*` writes via the built-in `print` (stdout substitute) until `os.Stdout` is available.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package ufmt provides utility functions for formatting strings, similarly to\n// the Go package \"fmt\", of which only a subset is currently supported.\npackage ufmt\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/ufmt/v0\"\ngno = \"0.9\"\n"},{"name":"ufmt.gno","body":"// Package ufmt provides utility functions for formatting strings, similarly to\n// the Go package \"fmt\", of which only a subset is currently supported (hence\n// the name µfmt - micro fmt). It includes functions like Printf, Sprintf,\n// Fprintf, and Errorf.\n// Supported formatting verbs are documented in the Sprintf function.\npackage ufmt\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// buffer accumulates formatted output as a byte slice.\ntype buffer []byte\n\nfunc (b *buffer) write(p []byte) {\n\t*b = append(*b, p...)\n}\n\nfunc (b *buffer) writeString(s string) {\n\t*b = append(*b, s...)\n}\n\nfunc (b *buffer) writeByte(c byte) {\n\t*b = append(*b, c)\n}\n\nfunc (b *buffer) writeRune(r rune) {\n\t*b = utf8.AppendRune(*b, r)\n}\n\n// printer holds state for formatting operations.\ntype printer struct {\n\tbuf buffer\n}\n\nfunc newPrinter() *printer {\n\treturn \u0026printer{}\n}\n\n// Sprint formats using the default formats for its operands and returns the resulting string.\n// Sprint writes the given arguments with spaces between arguments.\nfunc Sprint(a ...any) string {\n\tp := newPrinter()\n\tp.doPrint(a)\n\treturn string(p.buf)\n}\n\n// doPrint formats arguments using default formats and writes to printer's buffer.\n// Spaces are added between arguments.\nfunc (p *printer) doPrint(args []any) {\n\tfor argNum, arg := range args {\n\t\tif argNum \u003e 0 {\n\t\t\tp.buf.writeRune(' ')\n\t\t}\n\n\t\tswitch v := arg.(type) {\n\t\tcase string:\n\t\t\tp.buf.writeString(v)\n\t\tcase (interface{ String() string }):\n\t\t\tp.buf.writeString(v.String())\n\t\tcase error:\n\t\t\tp.buf.writeString(v.Error())\n\t\tcase float64:\n\t\t\tp.buf.writeString(Sprintf(\"%f\", v))\n\t\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t\tp.buf.writeString(Sprintf(\"%d\", v))\n\t\tcase bool:\n\t\t\tif v {\n\t\t\t\tp.buf.writeString(\"true\")\n\t\t\t} else {\n\t\t\t\tp.buf.writeString(\"false\")\n\t\t\t}\n\t\tcase nil:\n\t\t\tp.buf.writeString(\"\u003cnil\u003e\")\n\t\tdefault:\n\t\t\tp.buf.writeString(\"(unhandled)\")\n\t\t}\n\t}\n}\n\n// doPrintln appends a newline after formatting arguments with doPrint.\nfunc (p *printer) doPrintln(a []any) {\n\tp.doPrint(a)\n\tp.buf.writeByte('\\n')\n}\n\n// Sprintf offers similar functionality to Go's fmt.Sprintf, or the sprintf\n// equivalent available in many languages, including C/C++.\n// The number of args passed must exactly match the arguments consumed by the format.\n// A limited number of formatting verbs and features are currently supported.\n//\n// Supported verbs:\n//\n//\t%s: Places a string value directly.\n//\t    If the value implements the interface interface{ String() string },\n//\t    the String() method is called to retrieve the value. Same about Error()\n//\t    string.\n//\t%c: Formats the character represented by Unicode code point\n//\t%d: Formats an integer value using package \"strconv\".\n//\t    Currently supports only uint, uint64, int, int64.\n//\t%f: Formats a float value, with a default precision of 6.\n//\t%e: Formats a float with scientific notation; 1.23456e+78\n//\t%E: Formats a float with scientific notation; 1.23456E+78\n//\t%F: The same as %f\n//\t%g: Formats a float value with %e for large exponents, and %f with full precision for smaller numbers\n//\t%G: Formats a float value with %G for large exponents, and %F with full precision for smaller numbers\n//\t%t: Formats a boolean value to \"true\" or \"false\".\n//\t%x: Formats an integer value as a hexadecimal string.\n//\t    Currently supports only uint8, []uint8, [32]uint8.\n//\t%c: Formats a rune value as a string.\n//\t    Currently supports only rune, int.\n//\t%q: Formats a string value as a quoted string.\n//\t%T: Formats the type of the value.\n//\t%v: Formats the value with a default representation appropriate for the value's type\n//\t    - nil: \u003cnil\u003e\n//\t    - bool: true/false\n//\t    - integers: base 10\n//\t    - float64: %g format\n//\t    - string: verbatim\n//\t    - types with String()/Error(): method result\n//\t    - others: (unhandled)\n//\t%%: Outputs a literal %. Does not consume an argument.\n//\n// Unsupported verbs or type mismatches produce error strings like \"%!d(string=foo)\".\nfunc Sprintf(format string, a ...any) string {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\treturn string(p.buf)\n}\n\n// doPrintf parses the format string and writes formatted arguments to the buffer.\nfunc (p *printer) doPrintf(format string, args []any) {\n\tsTor := []rune(format)\n\tend := len(sTor)\n\targNum := 0\n\targLen := len(args)\n\n\tfor i := 0; i \u003c end; {\n\t\tisLast := i == end-1\n\t\tc := sTor[i]\n\n\t\tif isLast || c != '%' {\n\t\t\t// we don't check for invalid format like a one ending with \"%\"\n\t\t\tp.buf.writeRune(c)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tlength := -1\n\t\tprecision := -1\n\t\ti++ // skip '%'\n\n\t\tdigits := func() string {\n\t\t\tstart := i\n\t\t\tfor i \u003c end \u0026\u0026 sTor[i] \u003e= '0' \u0026\u0026 sTor[i] \u003c= '9' {\n\t\t\t\ti++\n\t\t\t}\n\t\t\tif i \u003e start {\n\t\t\t\treturn string(sTor[start:i])\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}\n\n\t\tif l := digits(); l != \"\" {\n\t\t\tvar err error\n\t\t\tlength, err = strconv.Atoi(l)\n\t\t\tif err != nil {\n\t\t\t\tpanic(\"ufmt: invalid length specification\")\n\t\t\t}\n\t\t}\n\n\t\tif i \u003c end \u0026\u0026 sTor[i] == '.' {\n\t\t\ti++ // skip '.'\n\t\t\tif l := digits(); l != \"\" {\n\t\t\t\tvar err error\n\t\t\t\tprecision, err = strconv.Atoi(l)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(\"ufmt: invalid precision specification\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif i \u003e= end {\n\t\t\tpanic(\"ufmt: invalid format string\")\n\t\t}\n\n\t\tverb := sTor[i]\n\t\tif verb == '%' {\n\t\t\tp.buf.writeRune('%')\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\tif argNum \u003e= argLen {\n\t\t\tpanic(\"ufmt: not enough arguments\")\n\t\t}\n\t\targ := args[argNum]\n\t\targNum++\n\n\t\tswitch verb {\n\t\tcase 'v':\n\t\t\twriteValue(p, verb, arg)\n\t\tcase 's':\n\t\t\twriteStringWithLength(p, verb, arg, length)\n\t\tcase 'c':\n\t\t\twriteChar(p, verb, arg)\n\t\tcase 'd':\n\t\t\twriteInt(p, verb, arg)\n\t\tcase 'e', 'E', 'f', 'F', 'g', 'G':\n\t\t\twriteFloatWithPrecision(p, verb, arg, precision)\n\t\tcase 't':\n\t\t\twriteBool(p, verb, arg)\n\t\tcase 'x':\n\t\t\twriteHex(p, verb, arg)\n\t\tcase 'q':\n\t\t\twriteQuotedString(p, verb, arg)\n\t\tcase 'T':\n\t\t\twriteType(p, arg)\n\t\t// % handled before, as it does not consume an argument\n\t\tdefault:\n\t\t\tp.buf.writeString(\"(unhandled verb: %\" + string(verb) + \")\")\n\t\t}\n\n\t\ti++\n\t}\n\n\tif argNum \u003c argLen {\n\t\tpanic(\"ufmt: too many arguments\")\n\t}\n}\n\n// writeValue handles %v formatting\nfunc writeValue(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase nil:\n\t\tp.buf.writeString(\"\u003cnil\u003e\")\n\tcase bool:\n\t\twriteBool(p, verb, v)\n\tcase int:\n\t\tp.buf.writeString(strconv.Itoa(v))\n\tcase int8:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int16:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int32:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int64:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase uint:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint8:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint16:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint32:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint64:\n\t\tp.buf.writeString(strconv.FormatUint(v, 10))\n\tcase float64:\n\t\tp.buf.writeString(strconv.FormatFloat(v, 'g', -1, 64))\n\tcase string:\n\t\tp.buf.writeString(v)\n\tcase []byte:\n\t\tp.buf.write(v)\n\tcase []rune:\n\t\tp.buf.writeString(string(v))\n\tcase (interface{ String() string }):\n\t\tp.buf.writeString(v.String())\n\tcase error:\n\t\tp.buf.writeString(v.Error())\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeStringWithLength handles %s formatting with length specification\nfunc writeStringWithLength(p *printer, verb rune, arg any, length int) {\n\tvar s string\n\tswitch v := arg.(type) {\n\tcase (interface{ String() string }):\n\t\ts = v.String()\n\tcase error:\n\t\ts = v.Error()\n\tcase string:\n\t\ts = v\n\tdefault:\n\t\ts = fallback(verb, v)\n\t}\n\n\tif length \u003e 0 \u0026\u0026 len(s) \u003c length {\n\t\ts = strings.Repeat(\" \", length-len(s)) + s\n\t}\n\tp.buf.writeString(s)\n}\n\n// writeChar handles %c formatting\nfunc writeChar(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\t// rune is int32. Exclude overflowing numeric types and dups (byte, int32):\n\tcase rune:\n\t\tp.buf.writeString(string(v))\n\tcase int:\n\t\tp.buf.writeRune(rune(v))\n\tcase int8:\n\t\tp.buf.writeRune(rune(v))\n\tcase int16:\n\t\tp.buf.writeRune(rune(v))\n\tcase uint:\n\t\tp.buf.writeRune(rune(v))\n\tcase uint8:\n\t\tp.buf.writeRune(rune(v))\n\tcase uint16:\n\t\tp.buf.writeRune(rune(v))\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeInt handles %d formatting\nfunc writeInt(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase int:\n\t\tp.buf.writeString(strconv.Itoa(v))\n\tcase int8:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int16:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int32:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase int64:\n\t\tp.buf.writeString(strconv.Itoa(int(v)))\n\tcase uint:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint8:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint16:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint32:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 10))\n\tcase uint64:\n\t\tp.buf.writeString(strconv.FormatUint(v, 10))\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeFloatWithPrecision handles floating-point formatting with precision\nfunc writeFloatWithPrecision(p *printer, verb rune, arg any, precision int) {\n\tswitch v := arg.(type) {\n\tcase float64:\n\t\tformat := byte(verb)\n\t\tif format == 'F' {\n\t\t\tformat = 'f'\n\t\t}\n\t\tif precision \u003c 0 {\n\t\t\tswitch format {\n\t\t\tcase 'e', 'E':\n\t\t\t\tprecision = 2\n\t\t\tdefault:\n\t\t\t\tprecision = 6\n\t\t\t}\n\t\t}\n\t\tp.buf = strconv.AppendFloat(p.buf, v, format, precision, 64)\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeBool handles %t formatting\nfunc writeBool(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase bool:\n\t\tif v {\n\t\t\tp.buf.writeString(\"true\")\n\t\t} else {\n\t\t\tp.buf.writeString(\"false\")\n\t\t}\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\n}\n\n// writeHex handles %x formatting\nfunc writeHex(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase uint8:\n\t\tp.buf.writeString(strconv.FormatUint(uint64(v), 16))\n\tdefault:\n\t\tp.buf.writeString(\"(unhandled)\")\n\t}\n}\n\n// writeQuotedString handles %q formatting\nfunc writeQuotedString(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase string:\n\t\tp.buf.writeString(strconv.Quote(v))\n\tdefault:\n\t\tp.buf.writeString(\"(unhandled)\")\n\t}\n}\n\n// writeType handles %T formatting\nfunc writeType(p *printer, arg any) {\n\tswitch arg.(type) {\n\tcase bool:\n\t\tp.buf.writeString(\"bool\")\n\tcase int:\n\t\tp.buf.writeString(\"int\")\n\tcase int8:\n\t\tp.buf.writeString(\"int8\")\n\tcase int16:\n\t\tp.buf.writeString(\"int16\")\n\tcase int32:\n\t\tp.buf.writeString(\"int32\")\n\tcase int64:\n\t\tp.buf.writeString(\"int64\")\n\tcase uint:\n\t\tp.buf.writeString(\"uint\")\n\tcase uint8:\n\t\tp.buf.writeString(\"uint8\")\n\tcase uint16:\n\t\tp.buf.writeString(\"uint16\")\n\tcase uint32:\n\t\tp.buf.writeString(\"uint32\")\n\tcase uint64:\n\t\tp.buf.writeString(\"uint64\")\n\tcase string:\n\t\tp.buf.writeString(\"string\")\n\tcase []byte:\n\t\tp.buf.writeString(\"[]byte\")\n\tcase []rune:\n\t\tp.buf.writeString(\"[]rune\")\n\tdefault:\n\t\tp.buf.writeString(\"unknown\")\n\t}\n}\n\n// Fprintf formats according to a format specifier and writes to w.\n// Returns the number of bytes written and any write error encountered.\nfunc Fprintf(w io.Writer, format string, a ...any) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\treturn w.Write(p.buf)\n}\n\n// Printf formats according to a format specifier and writes to standard output.\n// Returns the number of bytes written and any write error encountered.\n//\n// XXX: Replace with os.Stdout handling when available.\nfunc Printf(format string, a ...any) (n int, err error) {\n\tvar out strings.Builder\n\tn, err = Fprintf(\u0026out, format, a...)\n\tprint(out.String())\n\treturn n, err\n}\n\n// Appendf formats according to a format specifier, appends the result to the byte\n// slice, and returns the updated slice.\nfunc Appendf(b []byte, format string, a ...any) []byte {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\treturn append(b, p.buf...)\n}\n\n// Fprint formats using default formats and writes to w.\n// Spaces are added between arguments.\n// Returns the number of bytes written and any write error encountered.\nfunc Fprint(w io.Writer, a ...any) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrint(a)\n\treturn w.Write(p.buf)\n}\n\n// Print formats using default formats and writes to standard output.\n// Spaces are added between arguments.\n// Returns the number of bytes written and any write error encountered.\n//\n// XXX: Replace with os.Stdout handling when available.\nfunc Print(a ...any) (n int, err error) {\n\tvar out strings.Builder\n\tn, err = Fprint(\u0026out, a...)\n\tprint(out.String())\n\treturn n, err\n}\n\n// Append formats using default formats, appends to b, and returns the updated slice.\n// Spaces are added between arguments.\nfunc Append(b []byte, a ...any) []byte {\n\tp := newPrinter()\n\tp.doPrint(a)\n\treturn append(b, p.buf...)\n}\n\n// Fprintln formats using default formats and writes to w with newline.\n// Returns the number of bytes written and any write error encountered.\nfunc Fprintln(w io.Writer, a ...any) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\treturn w.Write(p.buf)\n}\n\n// Println formats using default formats and writes to standard output with newline.\n// Returns the number of bytes written and any write error encountered.\n//\n// XXX: Replace with os.Stdout handling when available.\nfunc Println(a ...any) (n int, err error) {\n\tvar out strings.Builder\n\tn, err = Fprintln(\u0026out, a...)\n\tprint(out.String())\n\treturn n, err\n}\n\n// Sprintln formats using default formats and returns the string with newline.\n// Spaces are always added between arguments.\nfunc Sprintln(a ...any) string {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\treturn string(p.buf)\n}\n\n// Appendln formats using default formats, appends to b, and returns the updated slice.\n// Appends a newline after the last argument.\nfunc Appendln(b []byte, a ...any) []byte {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\treturn append(b, p.buf...)\n}\n\n// This function is used to mimic Go's fmt.Sprintf\n// specific behaviour of showing verb/type mismatches,\n// where for example:\n//\n//\tfmt.Sprintf(\"%d\", \"foo\") gives \"%!d(string=foo)\"\n//\n// Here:\n//\n//\tfallback(\"s\", 8) -\u003e \"%!s(int=8)\"\n//\tfallback(\"d\", nil) -\u003e \"%!d(\u003cnil\u003e)\", and so on.f\nfunc fallback(verb rune, arg any) string {\n\tvar s string\n\tswitch v := arg.(type) {\n\tcase string:\n\t\ts = \"string=\" + v\n\tcase (interface{ String() string }):\n\t\ts = \"string=\" + v.String()\n\tcase error:\n\t\t// note: also \"string=\" in Go fmt\n\t\ts = \"string=\" + v.Error()\n\tcase float64:\n\t\ts = \"float64=\" + Sprintf(\"%f\", v)\n\tcase int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:\n\t\t// note: rune, byte would be dups, being aliases\n\t\tif typename, e := typeToString(v); e == nil {\n\t\t\ts = typename + \"=\" + Sprintf(\"%d\", v)\n\t\t} else {\n\t\t\tpanic(\"ufmt: unexpected type error\")\n\t\t}\n\tcase bool:\n\t\ts = \"bool=\" + strconv.FormatBool(v)\n\tcase nil:\n\t\ts = \"\u003cnil\u003e\"\n\tdefault:\n\t\ts = \"(unhandled)\"\n\t}\n\treturn \"%!\" + string(verb) + \"(\" + s + \")\"\n}\n\n// typeToString returns the name of basic Go types as string.\nfunc typeToString(v any) (string, error) {\n\tswitch v.(type) {\n\tcase string:\n\t\treturn \"string\", nil\n\tcase int:\n\t\treturn \"int\", nil\n\tcase int8:\n\t\treturn \"int8\", nil\n\tcase int16:\n\t\treturn \"int16\", nil\n\tcase int32:\n\t\treturn \"int32\", nil\n\tcase int64:\n\t\treturn \"int64\", nil\n\tcase uint:\n\t\treturn \"uint\", nil\n\tcase uint8:\n\t\treturn \"uint8\", nil\n\tcase uint16:\n\t\treturn \"uint16\", nil\n\tcase uint32:\n\t\treturn \"uint32\", nil\n\tcase uint64:\n\t\treturn \"uint64\", nil\n\tcase float32:\n\t\treturn \"float32\", nil\n\tcase float64:\n\t\treturn \"float64\", nil\n\tcase bool:\n\t\treturn \"bool\", nil\n\tdefault:\n\t\treturn \"\", errors.New(\"unsupported type\")\n\t}\n}\n\n// errMsg implements the error interface for formatted error strings.\ntype errMsg struct {\n\tmsg string\n}\n\n// Error returns the formatted error message.\nfunc (e *errMsg) Error() string {\n\treturn e.msg\n}\n\n// Errorf formats according to a format specifier and returns an error value.\n// Supports the same verbs as Sprintf. See Sprintf documentation for details.\nfunc Errorf(format string, args ...any) error {\n\treturn \u0026errMsg{Sprintf(format, args...)}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"avl","path":"gno.land/p/nt/avl/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `avl` - Gas-efficient AVL tree\n\nA self-balancing AVL tree for storing key-value data in Gno realms. Each node is persisted as a separate object, so operations only load `O(log n)` nodes from storage instead of the entire collection.\n\n## Usage\n\n```go\npackage myrealm\n\nimport \"gno.land/p/nt/avl/v0\"\n\n// Persisted across transactions.\nvar tree avl.Tree\n\nfunc Set(key string, value int) {\n    tree.Set(key, value)\n}\n\nfunc Get(key string) int {\n    // Get returns nil for an absent key. A stored nil value looks the same,\n    // so use Has when you must tell absent from present-but-nil.\n    raw := tree.Get(key)\n    if raw == nil {\n        panic(\"not found\")\n    }\n    return raw.(int)\n}\n\n// Iterate a bounded key range, stopping early when possible. Iterating\n// the whole tree with (\"\", \"\") loads every node (O(n) storage reads);\n// for large or user-growable trees, paginate with the pager subpackage.\nfunc ListRange(start, end string) {\n    tree.Iterate(start, end, func(key string, value any) bool {\n        // return true to stop early\n        return false\n    })\n}\n```\n\n## API\n\n```go\ntype Tree struct{ /* unexported */ }\n\nfunc NewTree() *Tree\n\n// Read\nfunc (t *Tree) Size() int\nfunc (t *Tree) Has(key string) bool\nfunc (t *Tree) Get(key string) (value any) // nil if the key is absent\nfunc (t *Tree) GetByIndex(index int) (key string, value any)\nfunc (t *Tree) Iterate(start, end string, cb IterCbFn) bool\nfunc (t *Tree) ReverseIterate(start, end string, cb IterCbFn) bool\nfunc (t *Tree) IterateByOffset(offset, count int, cb IterCbFn) bool\nfunc (t *Tree) ReverseIterateByOffset(offset, count int, cb IterCbFn) bool\n\n// Write\nfunc (t *Tree) Set(key string, value any) (updated bool)\nfunc (t *Tree) Remove(key string) (value any, removed bool)\n\ntype IterCbFn func(key string, value any) bool\n\ntype ITree interface { /* same shape as Tree's methods */ }\n```\n\nThe zero value of `Tree` is a usable empty tree. `Get` returns `nil` for an absent key, so use `Has` to distinguish a stored `nil` value from a missing one. `Iterate` uses `[start, end)` (start inclusive, end exclusive); empty strings mean unbounded. Callbacks return `true` to stop early.\n\n## Notes\n\n- `avl.Tree` and `bptree` (`gno.land/p/nt/bptree/v0`) expose the same `ITree` interface; bptree swaps AVL balancing for a B+ layout with better cache locality. `seqid` (`gno.land/p/nt/seqid/v0`) generates ordered keys usable in either.\n- Never return the live `*Tree` from a realm getter: a caller can then call `Set`/`Remove` on it under your realm's authority (readonly taint does not block method dispatch). Return values, copies, or a read-only `rotree` view.\n\n## Subpackages\n\n- `gno.land/p/nt/avl/v0/pager` - pagination helper for trees and lists.\n- `gno.land/p/nt/avl/v0/rotree` - read-only view of a `Tree`.\n\n## Why AVL over Map?\n\nIn Gno, the choice between `avl.Tree` and `map` is about how data is persisted.\n\n**Maps** are stored as a single monolithic object. Accessing *any* value loads the *entire* map. A map with 1,000 entries loads all 1,000 on every read.\n\n**AVL trees** store each node as a separate object. Accessing a value loads only the nodes along the search path — `~log2(n)`. A tree with 1,000 entries loads ~10 nodes; a tree with 1,000,000 entries still loads only ~20.\n\n### Storage comparison (1,000 entries)\n\n**Map:**\n\n```\nObject :4 = map{\n  (\"0\" string):(\"123\" string),\n  (\"1\" string):(\"123\" string),\n  ...\n  (\"999\" string):(\"123\" string)\n}\n```\n- `map[\"100\"]` loads object `:4` — all 1,000 pairs.\n- Gas cost proportional to total map size.\n\n**AVL tree:**\n\n```\nObject :6  = Node{key=\"4\",   height=10, size=1000, left=:7,  right=...}\nObject :9  = Node{key=\"2\",   height=9,  size=334,  left=:10, right=...}\nObject :11 = Node{key=\"14\",  height=8,  size=112,  left=:12, right=...}\nObject :13 = Node{key=\"12\",  height=6,  size=46,   left=:14, right=...}\nObject :15 = Node{key=\"11\",  height=5,  size=24,   left=:16, right=...}\nObject :17 = Node{key=\"102\", height=4,  size=13,   left=:18, right=...}\nObject :19 = Node{key=\"100\", height=3,  size=5,    left=:30, right=...}\nObject :31 = Node{key=\"101\", height=1,  size=2,    left=:32, right=...}\nObject :33 = Node{key=\"100\", value=\"123\", height=0, size=1}\n```\n- `tree.Get(\"100\")` loads ~10 objects (the search path only).\n- Gas cost proportional to `log2(n)`.\n\n## Further reading\n\n- [Why should you use an AVL tree instead of a map?](https://howl.moe/posts/2024-09-19-gno-avl-over-maps/)\n- [Berty's AVL scalability report](https://github.com/gnolang/hackerspace/issues/67) - testing up to 20M entries\n- [Effective Gno - Prefer avl.Tree over map](https://docs.gno.land/resources/effective-gno#prefer-avltree-over-map-for-scalable-storage)\n- [Wikipedia - AVL tree](https://en.wikipedia.org/wiki/AVL_tree)\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package avl provides a gas-efficient AVL tree implementation for storing\n// key-value data in Gno realms.\npackage avl\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/v0\"\ngno = \"0.9\"\n"},{"name":"node.gno","body":"package avl\n\n//----------------------------------------\n// Node\n\n// Node represents a node in an AVL tree.\ntype Node struct {\n\tkey       string // key is the unique identifier for the node.\n\tvalue     any    // value is the data stored in the node.\n\theight    int8   // height is the height of the node in the tree.\n\tsize      int    // size is the number of leaf nodes (key-value pairs) in the subtree rooted at this node.\n\tleftNode  *Node  // leftNode is the left child of the node.\n\trightNode *Node  // rightNode is the right child of the node.\n}\n\n// NewNode creates a new node with the given key and value.\nfunc NewNode(key string, value any) *Node {\n\treturn \u0026Node{\n\t\tkey:    key,\n\t\tvalue:  value,\n\t\theight: 0,\n\t\tsize:   1,\n\t}\n}\n\n// Size returns the size of the subtree rooted at the node.\nfunc (node *Node) Size() int {\n\tif node == nil {\n\t\treturn 0\n\t}\n\treturn node.size\n}\n\n// IsLeaf checks if the node is a leaf node (has no children).\nfunc (node *Node) IsLeaf() bool {\n\treturn node.height == 0\n}\n\n// Key returns the key of the node.\nfunc (node *Node) Key() string {\n\treturn node.key\n}\n\n// Value returns the value of the node.\nfunc (node *Node) Value() any {\n\treturn node.value\n}\n\nfunc (node *Node) _copy() *Node {\n\tif node.height == 0 {\n\t\tpanic(\"Why are you copying a value node?\")\n\t}\n\treturn \u0026Node{\n\t\tkey:       node.key,\n\t\theight:    node.height,\n\t\tsize:      node.size,\n\t\tleftNode:  node.leftNode,\n\t\trightNode: node.rightNode,\n\t}\n}\n\n// Has checks if a node with the given key exists in the subtree rooted at the node.\nfunc (node *Node) Has(key string) (has bool) {\n\tif node == nil {\n\t\treturn false\n\t}\n\tif node.key == key {\n\t\treturn true\n\t}\n\tif node.height == 0 {\n\t\treturn false\n\t} else {\n\t\tif key \u003c node.key {\n\t\t\treturn node.getLeftNode().Has(key)\n\t\t} else {\n\t\t\treturn node.getRightNode().Has(key)\n\t\t}\n\t}\n}\n\n// Get searches for a node with the given key in the subtree rooted at the node\n// and returns its index, value, and whether it exists.\nfunc (node *Node) Get(key string) (index int, value any, exists bool) {\n\tif node == nil {\n\t\treturn 0, nil, false\n\t}\n\n\tif node.height == 0 {\n\t\tif node.key == key {\n\t\t\treturn 0, node.value, true\n\t\t} else if node.key \u003c key {\n\t\t\treturn 1, nil, false\n\t\t} else {\n\t\t\treturn 0, nil, false\n\t\t}\n\t} else {\n\t\tif key \u003c node.key {\n\t\t\treturn node.getLeftNode().Get(key)\n\t\t} else {\n\t\t\trightNode := node.getRightNode()\n\t\t\tindex, value, exists = rightNode.Get(key)\n\t\t\tindex += node.size - rightNode.size\n\t\t\treturn index, value, exists\n\t\t}\n\t}\n}\n\n// GetByIndex retrieves the key-value pair of the node at the given index\n// in the subtree rooted at the node.\nfunc (node *Node) GetByIndex(index int) (key string, value any) {\n\tif index \u003c 0 {\n\t\tpanic(\"GetByIndex: negative index not allowed\")\n\t}\n\n\tif node.height == 0 {\n\t\tif index != 0 {\n\t\t\tpanic(\"GetByIndex asked for invalid index\")\n\t\t}\n\t\treturn node.key, node.value\n\t} else {\n\t\t// TODO: could improve this by storing the sizes\n\t\tleftNode := node.getLeftNode()\n\t\tif index \u003c leftNode.size {\n\t\t\treturn leftNode.GetByIndex(index)\n\t\t} else {\n\t\t\treturn node.getRightNode().GetByIndex(index - leftNode.size)\n\t\t}\n\t}\n}\n\n// Set inserts a new node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\n//\n// XXX consider a better way to do this... perhaps split Node from Node.\nfunc (node *Node) Set(key string, value any) (newSelf *Node, updated bool) {\n\tif node == nil {\n\t\treturn NewNode(key, value), false\n\t}\n\tif node.height == 0 {\n\t\tif key \u003c node.key {\n\t\t\treturn \u0026Node{\n\t\t\t\tkey:       node.key,\n\t\t\t\theight:    1,\n\t\t\t\tsize:      2,\n\t\t\t\tleftNode:  NewNode(key, value),\n\t\t\t\trightNode: node,\n\t\t\t}, false\n\t\t} else if key == node.key {\n\t\t\treturn NewNode(key, value), true\n\t\t} else {\n\t\t\treturn \u0026Node{\n\t\t\t\tkey:       key,\n\t\t\t\theight:    1,\n\t\t\t\tsize:      2,\n\t\t\t\tleftNode:  node,\n\t\t\t\trightNode: NewNode(key, value),\n\t\t\t}, false\n\t\t}\n\t} else {\n\t\tnode = node._copy()\n\t\tif key \u003c node.key {\n\t\t\tnode.leftNode, updated = node.getLeftNode().Set(key, value)\n\t\t} else {\n\t\t\tnode.rightNode, updated = node.getRightNode().Set(key, value)\n\t\t}\n\t\tif updated {\n\t\t\treturn node, updated\n\t\t} else {\n\t\t\tnode.calcHeightAndSize()\n\t\t\treturn node.balance(), updated\n\t\t}\n\t}\n}\n\n// Remove deletes the node with the given key from the subtree rooted at the node.\n// returns the new root of the subtree, the new leftmost leaf key (if changed),\n// the removed value and the removal was successful.\nfunc (node *Node) Remove(key string) (\n\tnewNode *Node, newKey string, value any, removed bool,\n) {\n\tif node == nil {\n\t\treturn nil, \"\", nil, false\n\t}\n\tif node.height == 0 {\n\t\tif key == node.key {\n\t\t\treturn nil, \"\", node.value, true\n\t\t} else {\n\t\t\treturn node, \"\", nil, false\n\t\t}\n\t} else {\n\t\tif key \u003c node.key {\n\t\t\tvar newLeftNode *Node\n\t\t\tnewLeftNode, newKey, value, removed = node.getLeftNode().Remove(key)\n\t\t\tif !removed {\n\t\t\t\treturn node, \"\", value, false\n\t\t\t} else if newLeftNode == nil { // left node held value, was removed\n\t\t\t\treturn node.rightNode, node.key, value, true\n\t\t\t}\n\t\t\tnode = node._copy()\n\t\t\tnode.leftNode = newLeftNode\n\t\t\tnode.calcHeightAndSize()\n\t\t\tnode = node.balance()\n\t\t\treturn node, newKey, value, true\n\t\t} else {\n\t\t\tvar newRightNode *Node\n\t\t\tnewRightNode, newKey, value, removed = node.getRightNode().Remove(key)\n\t\t\tif !removed {\n\t\t\t\treturn node, \"\", value, false\n\t\t\t} else if newRightNode == nil { // right node held value, was removed\n\t\t\t\treturn node.leftNode, \"\", value, true\n\t\t\t}\n\t\t\tnode = node._copy()\n\t\t\tnode.rightNode = newRightNode\n\t\t\tif newKey != \"\" {\n\t\t\t\tnode.key = newKey\n\t\t\t}\n\t\t\tnode.calcHeightAndSize()\n\t\t\tnode = node.balance()\n\t\t\treturn node, \"\", value, true\n\t\t}\n\t}\n}\n\nfunc (node *Node) getLeftNode() *Node {\n\treturn node.leftNode\n}\n\nfunc (node *Node) getRightNode() *Node {\n\treturn node.rightNode\n}\n\n// rotateRight performs a right rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateRight() *Node {\n\tnode = node._copy()\n\tl := node.getLeftNode()\n\t_l := l._copy()\n\n\t_lrCached := _l.rightNode\n\t_l.rightNode = node\n\tnode.leftNode = _lrCached\n\n\tnode.calcHeightAndSize()\n\t_l.calcHeightAndSize()\n\n\treturn _l\n}\n\n// rotateLeft performs a left rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateLeft() *Node {\n\tnode = node._copy()\n\tr := node.getRightNode()\n\t_r := r._copy()\n\n\t_rlCached := _r.leftNode\n\t_r.leftNode = node\n\tnode.rightNode = _rlCached\n\n\tnode.calcHeightAndSize()\n\t_r.calcHeightAndSize()\n\n\treturn _r\n}\n\n// calcHeightAndSize updates the height and size of the node based on its children.\n// NOTE: mutates height and size\nfunc (node *Node) calcHeightAndSize() {\n\tnode.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1\n\tnode.size = node.getLeftNode().size + node.getRightNode().size\n}\n\n// calcBalance calculates the balance factor of the node.\nfunc (node *Node) calcBalance() int {\n\treturn int(node.getLeftNode().height) - int(node.getRightNode().height)\n}\n\n// balance balances the subtree rooted at the node and returns the new root.\n// NOTE: assumes that node can be modified\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) balance() (newSelf *Node) {\n\tbalance := node.calcBalance()\n\tif balance \u003e 1 {\n\t\tif node.getLeftNode().calcBalance() \u003e= 0 {\n\t\t\t// Left Left Case\n\t\t\treturn node.rotateRight()\n\t\t} else {\n\t\t\t// Left Right Case\n\t\t\tleft := node.getLeftNode()\n\t\t\tnode.leftNode = left.rotateLeft()\n\t\t\treturn node.rotateRight()\n\t\t}\n\t}\n\tif balance \u003c -1 {\n\t\tif node.getRightNode().calcBalance() \u003c= 0 {\n\t\t\t// Right Right Case\n\t\t\treturn node.rotateLeft()\n\t\t} else {\n\t\t\t// Right Left Case\n\t\t\tright := node.getRightNode()\n\t\t\tnode.rightNode = right.rotateRight()\n\t\t\treturn node.rotateLeft()\n\t\t}\n\t}\n\t// Nothing changed\n\treturn node\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) Iterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, true, true, cb)\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, false, true, cb)\n}\n\n// TraverseInRange traverses all nodes, including inner nodes.\n// Start is inclusive and end is exclusive when ascending,\n// Start and end are inclusive when descending.\n// Empty start and empty end denote no start and no end.\n// If leavesOnly is true, only visit leaf nodes.\n// NOTE: To simulate an exclusive reverse traversal,\n// just append 0x00 to start.\nfunc (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\tafterStart := (start == \"\" || start \u003c node.key)\n\tstartOrAfter := (start == \"\" || start \u003c= node.key)\n\tbeforeEnd := false\n\tif ascending {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c end)\n\t} else {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c= end)\n\t}\n\n\t// Run callback per inner/leaf node.\n\tstop := false\n\tif (!node.IsLeaf() \u0026\u0026 !leavesOnly) ||\n\t\t(node.IsLeaf() \u0026\u0026 startOrAfter \u0026\u0026 beforeEnd) {\n\t\tstop = cb(node)\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t}\n\tif node.IsLeaf() {\n\t\treturn stop\n\t}\n\n\tif ascending {\n\t\t// check lower nodes, then higher\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t} else {\n\t\t// check the higher nodes first\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t}\n\n\treturn stop\n}\n\n// TraverseByOffset traverses all nodes, including inner nodes.\n// A limit of math.MaxInt means no limit.\nfunc (node *Node) TraverseByOffset(offset, limit int, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\n\t// Clamp negative offset to 0; otherwise `delta := first.size - offset`\n\t// over-counts and silently drops nodes.\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\n\t// fast paths. these happen only if TraverseByOffset is called directly on a leaf.\n\tif limit \u003c= 0 || offset \u003e= node.size {\n\t\treturn false\n\t}\n\tif node.IsLeaf() {\n\t\tif offset \u003e 0 {\n\t\t\treturn false\n\t\t}\n\t\treturn cb(node)\n\t}\n\n\t// go to the actual recursive function.\n\treturn node.traverseByOffset(offset, limit, ascending, leavesOnly, cb)\n}\n\n// TraverseByOffset traverses the subtree rooted at the node by offset and limit,\n// in either ascending or descending order, and applies the callback function to each traversed node.\n// If leavesOnly is true, only leaf nodes are visited.\nfunc (node *Node) traverseByOffset(offset, limit int, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\t// caller guarantees: offset \u003c node.size; limit \u003e 0.\n\tif !leavesOnly {\n\t\tif cb(node) {\n\t\t\treturn true // Stop traversal if callback returns true\n\t\t}\n\t}\n\tfirst, second := node.getLeftNode(), node.getRightNode()\n\tif !ascending {\n\t\tfirst, second = second, first\n\t}\n\tif first.IsLeaf() {\n\t\t// either run or skip, based on offset\n\t\tif offset \u003e 0 {\n\t\t\toffset--\n\t\t} else {\n\t\t\tif cb(first) {\n\t\t\t\treturn true // Stop traversal if callback returns true\n\t\t\t}\n\t\t\tlimit--\n\t\t\tif limit \u003c= 0 {\n\t\t\t\treturn true // Stop traversal when limit is reached\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// possible cases:\n\t\t// 1 the offset given skips the first node entirely\n\t\t// 2 the offset skips none or part of the first node, but the limit requires some of the second node.\n\t\t// 3 the offset skips none or part of the first node, and the limit stops our search on the first node.\n\t\tif offset \u003e= first.size {\n\t\t\toffset -= first.size // 1\n\t\t} else {\n\t\t\tif first.traverseByOffset(offset, limit, ascending, leavesOnly, cb) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// number of leaves which could actually be called from inside\n\t\t\tdelta := first.size - offset\n\t\t\toffset = 0\n\t\t\tif delta \u003e= limit {\n\t\t\t\treturn true // 3\n\t\t\t}\n\t\t\tlimit -= delta // 2\n\t\t}\n\t}\n\n\t// because of the caller guarantees and the way we handle the first node,\n\t// at this point we know that limit \u003e 0 and there must be some values in\n\t// this second node that we include.\n\n\t// =\u003e if the second node is a leaf, it has to be included.\n\tif second.IsLeaf() {\n\t\treturn cb(second)\n\t}\n\t// =\u003e if it is not a leaf, it will still be enough to recursively call this\n\t// function with the updated offset and limit\n\treturn second.traverseByOffset(offset, limit, ascending, leavesOnly, cb)\n}\n\n// Only used in testing...\nfunc (node *Node) lmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getLeftNode().lmd()\n}\n\n// Only used in testing...\nfunc (node *Node) rmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getRightNode().rmd()\n}\n\nfunc maxInt8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n"},{"name":"tree.gno","body":"package avl\n\ntype ITree interface {\n\t// read operations\n\n\tSize() int\n\tHas(key string) bool\n\tGet(key string) any\n\tGetByIndex(index int) (key string, value any)\n\tIterate(start, end string, cb IterCbFn) bool\n\tReverseIterate(start, end string, cb IterCbFn) bool\n\tIterateByOffset(offset int, count int, cb IterCbFn) bool\n\tReverseIterateByOffset(offset int, count int, cb IterCbFn) bool\n\n\t// write operations\n\n\tSet(key string, value any) (updated bool)\n\tRemove(key string) (value any, removed bool)\n}\n\ntype IterCbFn func(key string, value any) bool\n\n//----------------------------------------\n// Tree\n\n// The zero struct can be used as an empty tree.\ntype Tree struct {\n\tnode *Node\n}\n\n// NewTree creates a new empty AVL tree.\nfunc NewTree() *Tree {\n\treturn \u0026Tree{\n\t\tnode: nil,\n\t}\n}\n\n// Size returns the number of key-value pair in the tree.\nfunc (tree *Tree) Size() int {\n\treturn tree.node.Size()\n}\n\n// Has checks whether a key exists in the tree.\n// It returns true if the key exists, otherwise false.\nfunc (tree *Tree) Has(key string) (has bool) {\n\treturn tree.node.Has(key)\n}\n\n// Get retrieves the value associated with the given key.\n// It returns the value if the key exists, or nil if it doesn't.\n// Note that a key stored with a nil value is indistinguishable\n// from an absent key; use Has to check for existence.\n// This allows for a simpler usage pattern with type assertions:\n//\n//\tif value, ok := tree.Get(\"key\").(MyType); ok {\n//\t    // use value\n//\t}\nfunc (tree *Tree) Get(key string) any {\n\t_, value, _ := tree.node.Get(key)\n\treturn value\n}\n\n// GetByIndex retrieves the key-value pair at the specified index in the tree.\n// It returns the key and value at the given index.\nfunc (tree *Tree) GetByIndex(index int) (key string, value any) {\n\treturn tree.node.GetByIndex(index)\n}\n\n// Set inserts a key-value pair into the tree.\n// If the key already exists, the value will be updated.\n// It returns a boolean indicating whether the key was newly inserted or updated.\nfunc (tree *Tree) Set(key string, value any) (updated bool) {\n\tnewnode, updated := tree.node.Set(key, value)\n\ttree.node = newnode\n\treturn updated\n}\n\n// Remove removes a key-value pair from the tree.\n// It returns the removed value and a boolean indicating whether the key was found and removed.\nfunc (tree *Tree) Remove(key string) (value any, removed bool) {\n\tnewnode, _, value, removed := tree.node.Remove(key)\n\ttree.node = newnode\n\treturn value, removed\n}\n\n// Iterate performs an in-order traversal of the tree within the specified key range.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) Iterate(start, end string, cb IterCbFn) bool {\n\treturn tree.node.TraverseInRange(start, end, true, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool {\n\treturn tree.node.TraverseInRange(start, end, false, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// IterateByOffset performs an in-order traversal of the tree starting from the specified offset.\n// It calls the provided callback function for each key-value pair encountered, up to the specified count.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool {\n\treturn tree.node.TraverseByOffset(offset, count, true, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset.\n// It calls the provided callback function for each key-value pair encountered, up to the specified count.\n// If the callback returns true, the iteration is stopped.\nfunc (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool {\n\treturn tree.node.TraverseByOffset(offset, count, false, true,\n\t\tfunc(node *Node) bool {\n\t\t\treturn cb(node.Key(), node.Value())\n\t\t},\n\t)\n}\n\n// Verify that Tree implements TreeInterface\nvar _ ITree = (*Tree)(nil)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"addrset","path":"gno.land/p/moul/addrset","files":[{"name":"addrset.gno","body":"// Package addrset provides a specialized set data structure for managing unique Gno addresses.\n//\n// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order.\n// This package is particularly useful when you need to:\n//   - Track a collection of unique addresses (e.g., for whitelists, participants, etc.)\n//   - Efficiently check address membership\n//   - Support pagination when displaying addresses\n//\n// Example usage:\n//\n//\timport (\n//\t    \"gno.land/p/moul/addrset\"\n//\t)\n//\n//\tfunc MyHandler() {\n//\t    // Create a new address set\n//\t    var set addrset.Set\n//\n//\t    // Add some addresses\n//\t    addr1 := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n//\t    addr2 := address(\"g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth\")\n//\n//\t    set.Add(addr1)  // returns true (newly added)\n//\t    set.Add(addr2)  // returns true (newly added)\n//\t    set.Add(addr1)  // returns false (already exists)\n//\n//\t    // Check membership\n//\t    if set.Has(addr1) {\n//\t        // addr1 is in the set\n//\t    }\n//\n//\t    // Get size\n//\t    size := set.Size()  // returns 2\n//\n//\t    // Iterate with pagination (10 items per page, starting at offset 0)\n//\t    set.IterateByOffset(0, 10, func(addr address) bool {\n//\t        // Process addr\n//\t        return false  // continue iteration\n//\t    })\n//\n//\t    // Remove an address\n//\t    set.Remove(addr1)  // returns true (was present)\n//\t    set.Remove(addr1)  // returns false (not present)\n//\t}\npackage addrset\n\nimport \"gno.land/p/nt/avl/v0\"\n\ntype Set struct {\n\ttree avl.Tree\n}\n\n// Add inserts an address into the set.\n// Returns true if the address was newly added, false if it already existed.\nfunc (s *Set) Add(addr address) bool {\n\treturn !s.tree.Set(string(addr), nil)\n}\n\n// Remove deletes an address from the set.\n// Returns true if the address was found and removed, false if it didn't exist.\nfunc (s *Set) Remove(addr address) bool {\n\t_, removed := s.tree.Remove(string(addr))\n\treturn removed\n}\n\n// Has checks if an address exists in the set.\nfunc (s *Set) Has(addr address) bool {\n\treturn s.tree.Has(string(addr))\n}\n\n// Size returns the number of addresses in the set.\nfunc (s *Set) Size() int {\n\treturn s.tree.Size()\n}\n\n// IterateByOffset walks through addresses starting at the given offset.\n// The callback should return true to stop iteration.\nfunc (s *Set) IterateByOffset(offset int, count int, cb func(addr address) bool) {\n\ts.tree.IterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(address(key))\n\t})\n}\n\n// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset.\n// The callback should return true to stop iteration.\nfunc (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr address) bool) {\n\ts.tree.ReverseIterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(address(key))\n\t})\n}\n\n// Tree returns the underlying AVL tree for advanced usage.\nfunc (s *Set) Tree() avl.ITree {\n\treturn \u0026s.tree\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/addrset\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"readonly.gno","body":"package addrset\n\n// ReadonlySet is a read-only view of a *Set. Cross-package callers cannot\n// mutate the underlying set through this type: it exposes no mutator\n// methods and holds the *Set in an unexported field, so a foreign realm\n// can neither reach the set nor invoke Add/Remove on it.\n//\n// A ReadonlySet is a thin handle over the live Set (it does not copy or\n// snapshot), so reads through it always reflect the Set's current contents.\ntype ReadonlySet struct {\n\tset *Set\n}\n\n// NewReadonlySet returns a read-only view of s.\nfunc NewReadonlySet(s *Set) *ReadonlySet {\n\treturn \u0026ReadonlySet{set: s}\n}\n\n// Readonly returns a read-only view of the set.\nfunc (s *Set) Readonly() *ReadonlySet {\n\treturn NewReadonlySet(s)\n}\n\n// Has reports whether addr is in the underlying set.\nfunc (r ReadonlySet) Has(addr address) bool {\n\treturn r.set.Has(addr)\n}\n\n// Size returns the number of addresses in the underlying set.\nfunc (r ReadonlySet) Size() int {\n\treturn r.set.Size()\n}\n\n// IterateByOffset walks the underlying set in sorted order, starting at\n// offset and visiting up to count addresses. fn returns true to stop early;\n// IterateByOffset returns true if iteration was stopped that way.\n//\n// The wrapped Set.IterateByOffset has no return value, so the \"stopped\"\n// result is synthesized from the last callback return via a\n// closure-captured local.\nfunc (r ReadonlySet) IterateByOffset(offset, count int, fn func(addr address) bool) (stopped bool) {\n\tr.set.IterateByOffset(offset, count, func(a address) bool {\n\t\tstopped = fn(a)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// ReverseIterateByOffset is IterateByOffset in reverse (descending) order.\nfunc (r ReadonlySet) ReverseIterateByOffset(offset, count int, fn func(addr address) bool) (stopped bool) {\n\tr.set.ReverseIterateByOffset(offset, count, func(a address) bool {\n\t\tstopped = fn(a)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"rotree","path":"gno.land/p/nt/bptree/v0/rotree","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/v0/rotree\"\ngno = \"0.9\"\n"},{"name":"rotree.gno","body":"// Package rotree provides a read-only wrapper for bptree.BPTree with safe value transformation.\n//\n// It is useful when you want to expose a read-only view of a tree while ensuring that\n// the sensitive data cannot be modified.\n//\n// Example:\n//\n//\t// Define a user structure with sensitive data\n//\ttype User struct {\n//\t\tName     string\n//\t\tBalance  int\n//\t\tInternal string // sensitive field\n//\t}\n//\n//\t// Create and populate the original tree\n//\tprivateTree := bptree.NewBPTree32()\n//\tprivateTree.Set(\"alice\", \u0026User{\n//\t\tName:     \"Alice\",\n//\t\tBalance:  100,\n//\t\tInternal: \"sensitive\",\n//\t})\n//\n//\t// Create a safe transformation function that copies the struct\n//\t// while excluding sensitive data\n//\tmakeEntrySafeFn := func(v any) any {\n//\t\tu := v.(*User)\n//\t\treturn \u0026User{\n//\t\t\tName:     u.Name,\n//\t\t\tBalance:  u.Balance,\n//\t\t\tInternal: \"\", // omit sensitive data\n//\t\t}\n//\t}\n//\n//\t// Create a read-only view of the tree\n//\tPublicTree := rotree.Wrap(tree, makeEntrySafeFn)\n//\n//\t// Safely access the data\n//\tvalue := roTree.Get(\"alice\")\n//\tuser := value.(*User)\n//\t// user.Name == \"Alice\"\n//\t// user.Balance == 100\n//\t// user.Internal == \"\" (sensitive data is filtered)\npackage rotree\n\nimport (\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\n// Wrap creates a new ReadOnlyTree from an existing bptree.BPTree and a safety transformation function.\n// If makeEntrySafeFn is nil, values will be returned as-is without transformation.\nfunc Wrap(tree *bptree.BPTree, makeEntrySafeFn func(any) any) *ReadOnlyTree {\n\treturn \u0026ReadOnlyTree{\n\t\ttree:            tree,\n\t\tmakeEntrySafeFn: makeEntrySafeFn,\n\t}\n}\n\n// ReadOnlyTree wraps a bptree.BPTree and provides read-only access.\ntype ReadOnlyTree struct {\n\ttree            *bptree.BPTree\n\tmakeEntrySafeFn func(any) any\n}\n\n// IReadOnlyTree defines the read-only operations available on a tree.\ntype IReadOnlyTree interface {\n\tSize() int\n\tHas(key string) bool\n\tGet(key string) any\n\tGetByIndex(index int) (string, any)\n\tIterate(start, end string, cb bptree.IterCbFn) bool\n\tReverseIterate(start, end string, cb bptree.IterCbFn) bool\n\tIterateByOffset(offset int, count int, cb bptree.IterCbFn) bool\n\tReverseIterateByOffset(offset int, count int, cb bptree.IterCbFn) bool\n}\n\n// Verify that ReadOnlyTree implements both ITree and IReadOnlyTree\nvar (\n\t_ bptree.ITree  = (*ReadOnlyTree)(nil)\n\t_ IReadOnlyTree = (*ReadOnlyTree)(nil)\n)\n\n// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value\nfunc (roTree *ReadOnlyTree) getSafeValue(value any) any {\n\tif roTree.makeEntrySafeFn == nil {\n\t\treturn value\n\t}\n\treturn roTree.makeEntrySafeFn(value)\n}\n\n// Size returns the number of key-value pairs in the tree.\nfunc (roTree *ReadOnlyTree) Size() int {\n\treturn roTree.tree.Size()\n}\n\n// Has checks whether a key exists in the tree.\nfunc (roTree *ReadOnlyTree) Has(key string) bool {\n\treturn roTree.tree.Has(key)\n}\n\n// Get retrieves the value associated with the given key, converted to a safe format.\n// It returns the value if the key exists, or nil if it doesn't.\nfunc (roTree *ReadOnlyTree) Get(key string) any {\n\tvalue := roTree.tree.Get(key)\n\tif value == nil {\n\t\treturn nil\n\t}\n\treturn roTree.getSafeValue(value)\n}\n\n// GetByIndex retrieves the key-value pair at the specified index in the tree, with the value converted to a safe format.\nfunc (roTree *ReadOnlyTree) GetByIndex(index int) (string, any) {\n\tkey, value := roTree.tree.GetByIndex(index)\n\treturn key, roTree.getSafeValue(value)\n}\n\n// Iterate performs an in-order traversal of the tree within the specified key range.\nfunc (roTree *ReadOnlyTree) Iterate(start, end string, cb bptree.IterCbFn) bool {\n\treturn roTree.tree.Iterate(start, end, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range.\nfunc (roTree *ReadOnlyTree) ReverseIterate(start, end string, cb bptree.IterCbFn) bool {\n\treturn roTree.tree.ReverseIterate(start, end, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// IterateByOffset performs an in-order traversal of the tree starting from the specified offset.\nfunc (roTree *ReadOnlyTree) IterateByOffset(offset int, count int, cb bptree.IterCbFn) bool {\n\treturn roTree.tree.IterateByOffset(offset, count, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset.\nfunc (roTree *ReadOnlyTree) ReverseIterateByOffset(offset int, count int, cb bptree.IterCbFn) bool {\n\treturn roTree.tree.ReverseIterateByOffset(offset, count, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// Set is not supported on ReadOnlyTree and will panic.\nfunc (roTree *ReadOnlyTree) Set(key string, value any) bool {\n\tpanic(\"Set operation not supported on ReadOnlyTree\")\n}\n\n// Remove is not supported on ReadOnlyTree and will panic.\nfunc (roTree *ReadOnlyTree) Remove(key string) (value any, removed bool) {\n\tpanic(\"Remove operation not supported on ReadOnlyTree\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"cford32","path":"gno.land/p/nt/cford32/v0","files":[{"name":"LICENSE","body":"Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"},{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `cford32` - Crockford Base32 encoding\n\nModified base32 encoding using the [Crockford alphabet](https://www.crockford.com/base32.html). Designed to be human-readable, error-resistant, and pronounceable: the ambiguous characters `I`, `L`, `O`, `U` are excluded from the encoding, and decoding accepts `I`/`L` as `1` and `O` as `0`. Output is never padded.\n\n## Usage\n\n```go\nimport \"gno.land/p/nt/cford32/v0\"\n\n// Byte slice encode/decode.\nencoded := cford32.EncodeToString([]byte(\"hello\"))  // uppercase, no padding\ndecoded, err := cford32.DecodeString(encoded)        // []byte(\"hello\")\n\n// Lowercase variant.\nlower := cford32.EncodeToStringLower([]byte(\"hello\"))\n\n// Compact uint64 encoding: 7 bytes for id \u003c 2^34, else 13 bytes.\nenc := cford32.PutCompact(42)\nback, _ := cford32.Uint64(enc) // 42\n\n// Full fixed-width uint64 encoding (always 13 bytes).\nfull := cford32.PutUint64(42)\n```\n\n## API\n\n```go\n// Errors.\ntype CorruptInputError int64\nfunc (e CorruptInputError) Error() string\n\n// Length helpers.\nfunc DecodedLen(n int) int\nfunc EncodedLen(n int) int\n\n// Byte slice encoding.\nfunc Encode(dst, src []byte)                          // uppercase\nfunc EncodeLower(dst, src []byte)                     // lowercase\nfunc EncodeToString(src []byte) string                // uppercase\nfunc EncodeToStringLower(src []byte) string           // lowercase\nfunc AppendEncode(dst, src []byte) []byte\nfunc AppendEncodeLower(dst, src []byte) []byte\n\n// Byte slice decoding. Case-insensitive; ignores \\r and \\n.\nfunc Decode(dst, src []byte) (n int, err error)\nfunc DecodeString(s string) ([]byte, error)\nfunc AppendDecode(dst, src []byte) ([]byte, error)\n\n// uint64 encoding.\nfunc PutUint64(id uint64) [13]byte                    // full, uppercase\nfunc PutUint64Lower(id uint64) [13]byte               // full, lowercase\nfunc PutCompact(id uint64) []byte                     // 7 bytes if id \u003c 2^34, else 13, lowercase\nfunc AppendCompact(id uint64, b []byte) []byte\nfunc Uint64(b []byte) (uint64, error)                 // accepts both compact (7) and full (13)\n\n// Streaming I/O.\nfunc NewEncoder(w io.Writer) io.WriteCloser\nfunc NewEncoderLower(w io.Writer) io.WriteCloser\nfunc NewDecoder(r io.Reader) io.Reader\n```\n\n## Notes\n\n- Alphabet: `0123456789ABCDEFGHJKMNPQRSTVWXYZ` (no `I`, `L`, `O`, `U`).\n- Decoding is case-insensitive; `I`/`i`/`L`/`l` decode as `1`, and `O`/`o` decode as `0`.\n- The compact uint64 encoding preserves lexicographic order with numeric order, making encoded IDs suitable as ordered keys.\n- The compact and full uint64 encodings are unambiguously distinguished by their first character: `0`-`f` indicates compact (7 bytes), `g`-`z` indicates full (13 bytes).\n- Values in `[0, 2^34)` have BOTH a compact and a full encoding. Pick one scheme per key space and stick to it: mixing both for the same value breaks the lexicographic-order property. `PutCompact` rolls over from compact to full at `2^34` automatically, which is safe as long as everything in that space is generated the same way.\n- For sequential IDs, see [`gno.land/p/nt/seqid/v0`](../../seqid/v0).\n"},{"name":"cford32.gno","body":"// Modified from the Go Source code for encoding/base32.\n// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package cford32 implements a base32-like encoding/decoding package, with the\n// encoding scheme [specified by Douglas Crockford].\n//\n// From the website, the requirements of said encoding scheme are to:\n//\n//   - Be human readable and machine readable.\n//   - Be compact. Humans have difficulty in manipulating long strings of arbitrary symbols.\n//   - Be error resistant. Entering the symbols must not require keyboarding gymnastics.\n//   - Be pronounceable. Humans should be able to accurately transmit the symbols to other humans using a telephone.\n//\n// This is slightly different from a simple difference in encoding table from\n// the Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\n// parsed as 1, and o O is parsed as 0.\n//\n// This package additionally provides ways to encode uint64's efficiently,\n// as well as efficient encoding to a lowercase variation of the encoding.\n// The encodings never use paddings.\n//\n// # Uint64 Encoding\n//\n// Aside from lower/uppercase encoding, there is a compact encoding, allowing\n// to encode all values in [0,2^34), and the full encoding, allowing all\n// values in [0,2^64). The compact encoding uses 7 characters, and the full\n// encoding uses 13 characters. Both are parsed unambiguously by the Uint64\n// decoder.\n//\n// The compact encodings have the first character between ['0','f'], while the\n// full encoding's first character ranges between ['g','z']. Practically, in\n// your usage of the package, you should consider which one to use and stick\n// with it, while considering that the compact encoding, once it reaches 2^34,\n// automatically switches to the full encoding. The properties of the generated\n// strings are still maintained: for instance, any two encoded uint64s x,y\n// consistently generated with the compact encoding, if the numeric value is\n// x \u003c y, will also be x \u003c y in lexical ordering. However, values [0,2^34) have a\n// \"double encoding\", which if mixed together lose the lexical ordering property.\n//\n// The Uint64 encoding is most useful for generating string versions of Uint64\n// IDs. Practically, it allows you to retain sleek and compact IDs for your\n// application for the first 2^34 (\u003e17 billion) entities, while seamlessly\n// rolling over to the full encoding should you exceed that. You are encouraged\n// to use it unless you have a requirement or preferences for IDs consistently\n// being always the same size.\n//\n// To use the cford32 encoding for IDs, you may want to consider using package\n// [gno.land/p/nt/seqid/v0].\n//\n// [specified by Douglas Crockford]: https://www.crockford.com/base32.html\npackage cford32\n\nimport (\n\t\"io\"\n\t\"strconv\"\n)\n\nconst (\n\tencTable      = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\"\n\tencTableLower = \"0123456789abcdefghjkmnpqrstvwxyz\"\n\n\t// each line is 16 bytes\n\tdecTable = \"\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 00-0f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 10-1f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 20-2f\n\t\t\"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\xff\\xff\\xff\\xff\\xff\\xff\" + // 30-3f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 40-4f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 50-5f\n\t\t\"\\xff\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x01\\x12\\x13\\x01\\x14\\x15\\x00\" + // 60-6f\n\t\t\"\\x16\\x17\\x18\\x19\\x1a\\xff\\x1b\\x1c\\x1d\\x1e\\x1f\\xff\\xff\\xff\\xff\\xff\" + // 70-7f\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" + // 80-ff (not ASCII)\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" +\n\t\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n)\n\n// CorruptInputError is returned by parsing functions when an invalid character\n// in the input is found. The integer value represents the byte index where\n// the error occurred.\n//\n// This is typically because the given character does not exist in the encoding.\ntype CorruptInputError int64\n\nfunc (e CorruptInputError) Error() string {\n\treturn \"illegal cford32 data at input byte \" + strconv.FormatInt(int64(e), 10)\n}\n\n// Uint64 parses a cford32-encoded byte slice into a uint64.\n//\n//   - The parser requires all provided character to be valid cford32 characters.\n//   - The parser disregards case.\n//   - If the first character is '0' \u003c= c \u003c= 'f', then the passed value is assumed\n//     encoded in the compact encoding, and must be 7 characters long.\n//   - If the first character is 'g' \u003c= c \u003c= 'z',  then the passed value is\n//     assumed encoded in the full encoding, and must be 13 characters long.\n//\n// If any of these requirements fail, a CorruptInputError will be returned.\nfunc Uint64(b []byte) (uint64, error) {\n\tif len(b) == 0 {\n\t\treturn 0, CorruptInputError(0)\n\t}\n\tb0 := decTable[b[0]]\n\tswitch {\n\tdefault:\n\t\treturn 0, CorruptInputError(0)\n\tcase len(b) == 7 \u0026\u0026 b0 \u003c 16:\n\t\tdecVals := [7]byte{\n\t\t\tdecTable[b[0]],\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c30 |\n\t\t\tuint64(decVals[1])\u003c\u003c25 |\n\t\t\tuint64(decVals[2])\u003c\u003c20 |\n\t\t\tuint64(decVals[3])\u003c\u003c15 |\n\t\t\tuint64(decVals[4])\u003c\u003c10 |\n\t\t\tuint64(decVals[5])\u003c\u003c5 |\n\t\t\tuint64(decVals[6]), nil\n\tcase len(b) == 13 \u0026\u0026 b0 \u003e= 16 \u0026\u0026 b0 \u003c 32:\n\t\tdecVals := [13]byte{\n\t\t\tdecTable[b[0]] \u0026 0x0F, // disregard high bit\n\t\t\tdecTable[b[1]],\n\t\t\tdecTable[b[2]],\n\t\t\tdecTable[b[3]],\n\t\t\tdecTable[b[4]],\n\t\t\tdecTable[b[5]],\n\t\t\tdecTable[b[6]],\n\t\t\tdecTable[b[7]],\n\t\t\tdecTable[b[8]],\n\t\t\tdecTable[b[9]],\n\t\t\tdecTable[b[10]],\n\t\t\tdecTable[b[11]],\n\t\t\tdecTable[b[12]],\n\t\t}\n\t\tfor idx, v := range decVals {\n\t\t\tif v \u003e= 32 {\n\t\t\t\treturn 0, CorruptInputError(idx)\n\t\t\t}\n\t\t}\n\n\t\treturn 0 +\n\t\t\tuint64(decVals[0])\u003c\u003c60 |\n\t\t\tuint64(decVals[1])\u003c\u003c55 |\n\t\t\tuint64(decVals[2])\u003c\u003c50 |\n\t\t\tuint64(decVals[3])\u003c\u003c45 |\n\t\t\tuint64(decVals[4])\u003c\u003c40 |\n\t\t\tuint64(decVals[5])\u003c\u003c35 |\n\t\t\tuint64(decVals[6])\u003c\u003c30 |\n\t\t\tuint64(decVals[7])\u003c\u003c25 |\n\t\t\tuint64(decVals[8])\u003c\u003c20 |\n\t\t\tuint64(decVals[9])\u003c\u003c15 |\n\t\t\tuint64(decVals[10])\u003c\u003c10 |\n\t\t\tuint64(decVals[11])\u003c\u003c5 |\n\t\t\tuint64(decVals[12]), nil\n\t}\n}\n\nconst mask = 31\n\n// PutUint64 returns a cford32-encoded byte slice.\nfunc PutUint64(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTable[id\u003e\u003e60\u0026mask|0x10], // specify full encoding\n\t\tencTable[id\u003e\u003e55\u0026mask],\n\t\tencTable[id\u003e\u003e50\u0026mask],\n\t\tencTable[id\u003e\u003e45\u0026mask],\n\t\tencTable[id\u003e\u003e40\u0026mask],\n\t\tencTable[id\u003e\u003e35\u0026mask],\n\t\tencTable[id\u003e\u003e30\u0026mask],\n\t\tencTable[id\u003e\u003e25\u0026mask],\n\t\tencTable[id\u003e\u003e20\u0026mask],\n\t\tencTable[id\u003e\u003e15\u0026mask],\n\t\tencTable[id\u003e\u003e10\u0026mask],\n\t\tencTable[id\u003e\u003e5\u0026mask],\n\t\tencTable[id\u0026mask],\n\t}\n}\n\n// PutUint64Lower returns a cford32-encoded byte array, swapping uppercase\n// letters with lowercase.\n//\n// For more information on how the value is encoded, see [Uint64].\nfunc PutUint64Lower(id uint64) [13]byte {\n\treturn [13]byte{\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t}\n}\n\n// PutCompact returns a cford32-encoded byte slice, using the compact\n// representation of cford32 described in the package documentation where\n// possible (all values of id \u003c 1\u003c\u003c34). The lowercase encoding is used.\n//\n// The resulting byte slice will be 7 bytes long for all compact values,\n// and 13 bytes long for\nfunc PutCompact(id uint64) []byte {\n\treturn AppendCompact(id, nil)\n}\n\n// AppendCompact works like [PutCompact] but appends to the given byte slice\n// instead of allocating one anew.\nfunc AppendCompact(id uint64, b []byte) []byte {\n\tconst maxCompact = 1 \u003c\u003c 34\n\tif id \u003c maxCompact {\n\t\treturn append(b,\n\t\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\t\tencTableLower[id\u0026mask],\n\t\t)\n\t}\n\treturn append(b,\n\t\tencTableLower[id\u003e\u003e60\u0026mask|0x10],\n\t\tencTableLower[id\u003e\u003e55\u0026mask],\n\t\tencTableLower[id\u003e\u003e50\u0026mask],\n\t\tencTableLower[id\u003e\u003e45\u0026mask],\n\t\tencTableLower[id\u003e\u003e40\u0026mask],\n\t\tencTableLower[id\u003e\u003e35\u0026mask],\n\t\tencTableLower[id\u003e\u003e30\u0026mask],\n\t\tencTableLower[id\u003e\u003e25\u0026mask],\n\t\tencTableLower[id\u003e\u003e20\u0026mask],\n\t\tencTableLower[id\u003e\u003e15\u0026mask],\n\t\tencTableLower[id\u003e\u003e10\u0026mask],\n\t\tencTableLower[id\u003e\u003e5\u0026mask],\n\t\tencTableLower[id\u0026mask],\n\t)\n}\n\nfunc DecodedLen(n int) int {\n\treturn n/8*5 + n%8*5/8\n}\n\nfunc EncodedLen(n int) int {\n\treturn n/5*8 + (n%5*8+4)/5\n}\n\n// Encode encodes src using the encoding enc,\n// writing [EncodedLen](len(src)) bytes to dst.\n//\n// The encoding does not contain any padding, unlike Go's base32.\nfunc Encode(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTable[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTable[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTable[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTable[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTable[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTable[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTable[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTable[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTable[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTable[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTable[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTable[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTable[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTable[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTable[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// EncodeLower is like [Encode], but uses the lowercase\nfunc EncodeLower(dst, src []byte) {\n\t// Copied from encoding/base32/base32.go (go1.22)\n\tif len(src) == 0 {\n\t\treturn\n\t}\n\n\tdi, si := 0, 0\n\tn := (len(src) / 5) * 5\n\tfor si \u003c n {\n\t\t// Combining two 32 bit loads allows the same code to be used\n\t\t// for 32 and 64 bit platforms.\n\t\thi := uint32(src[si+0])\u003c\u003c24 | uint32(src[si+1])\u003c\u003c16 | uint32(src[si+2])\u003c\u003c8 | uint32(src[si+3])\n\t\tlo := hi\u003c\u003c8 | uint32(src[si+4])\n\n\t\tdst[di+0] = encTableLower[(hi\u003e\u003e27)\u00260x1F]\n\t\tdst[di+1] = encTableLower[(hi\u003e\u003e22)\u00260x1F]\n\t\tdst[di+2] = encTableLower[(hi\u003e\u003e17)\u00260x1F]\n\t\tdst[di+3] = encTableLower[(hi\u003e\u003e12)\u00260x1F]\n\t\tdst[di+4] = encTableLower[(hi\u003e\u003e7)\u00260x1F]\n\t\tdst[di+5] = encTableLower[(hi\u003e\u003e2)\u00260x1F]\n\t\tdst[di+6] = encTableLower[(lo\u003e\u003e5)\u00260x1F]\n\t\tdst[di+7] = encTableLower[(lo)\u00260x1F]\n\n\t\tsi += 5\n\t\tdi += 8\n\t}\n\n\t// Add the remaining small block\n\tremain := len(src) - si\n\tif remain == 0 {\n\t\treturn\n\t}\n\n\t// Encode the remaining bytes in reverse order.\n\tval := uint32(0)\n\tswitch remain {\n\tcase 4:\n\t\tval |= uint32(src[si+3])\n\t\tdst[di+6] = encTableLower[val\u003c\u003c3\u00260x1F]\n\t\tdst[di+5] = encTableLower[val\u003e\u003e2\u00260x1F]\n\t\tfallthrough\n\tcase 3:\n\t\tval |= uint32(src[si+2]) \u003c\u003c 8\n\t\tdst[di+4] = encTableLower[val\u003e\u003e7\u00260x1F]\n\t\tfallthrough\n\tcase 2:\n\t\tval |= uint32(src[si+1]) \u003c\u003c 16\n\t\tdst[di+3] = encTableLower[val\u003e\u003e12\u00260x1F]\n\t\tdst[di+2] = encTableLower[val\u003e\u003e17\u00260x1F]\n\t\tfallthrough\n\tcase 1:\n\t\tval |= uint32(src[si+0]) \u003c\u003c 24\n\t\tdst[di+1] = encTableLower[val\u003e\u003e22\u00260x1F]\n\t\tdst[di+0] = encTableLower[val\u003e\u003e27\u00260x1F]\n\t}\n}\n\n// AppendEncode appends the cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncode(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncode(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\n// AppendEncodeLower appends the lowercase cford32 encoded src to dst\n// and returns the extended buffer.\nfunc AppendEncodeLower(dst, src []byte) []byte {\n\tn := EncodedLen(len(src))\n\tdst = grow(dst, n)\n\tEncodeLower(dst[len(dst):][:n], src)\n\treturn dst[:len(dst)+n]\n}\n\nfunc grow(s []byte, n int) []byte {\n\t// slices.Grow\n\tif n -= cap(s) - len(s); n \u003e 0 {\n\t\tnews := make([]byte, cap(s)+n)\n\t\tcopy(news[:cap(s)], s[:cap(s)])\n\t\treturn news[:len(s)]\n\t}\n\treturn s\n}\n\n// EncodeToString returns the cford32 encoding of src.\nfunc EncodeToString(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncode(buf, src)\n\treturn string(buf)\n}\n\n// EncodeToStringLower returns the cford32 lowercase encoding of src.\nfunc EncodeToStringLower(src []byte) string {\n\tbuf := make([]byte, EncodedLen(len(src)))\n\tEncodeLower(buf, src)\n\treturn string(buf)\n}\n\nfunc decode(dst, src []byte) (n int, err error) {\n\tdsti := 0\n\tolen := len(src)\n\n\tfor len(src) \u003e 0 {\n\t\t// Decode quantum using the base32 alphabet\n\t\tvar dbuf [8]byte\n\t\tdlen := 8\n\n\t\tfor j := 0; j \u003c 8; {\n\t\t\tif len(src) == 0 {\n\t\t\t\t// We have reached the end and are not expecting any padding\n\t\t\t\tdlen = j\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tin := src[0]\n\t\t\tsrc = src[1:]\n\t\t\tdbuf[j] = decTable[in]\n\t\t\tif dbuf[j] == 0xFF {\n\t\t\t\treturn n, CorruptInputError(olen - len(src) - 1)\n\t\t\t}\n\t\t\tj++\n\t\t}\n\n\t\t// Pack 8x 5-bit source blocks into 5 byte destination\n\t\t// quantum\n\t\tswitch dlen {\n\t\tcase 8:\n\t\t\tdst[dsti+4] = dbuf[6]\u003c\u003c5 | dbuf[7]\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 7:\n\t\t\tdst[dsti+3] = dbuf[4]\u003c\u003c7 | dbuf[5]\u003c\u003c2 | dbuf[6]\u003e\u003e3\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 5:\n\t\t\tdst[dsti+2] = dbuf[3]\u003c\u003c4 | dbuf[4]\u003e\u003e1\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 4:\n\t\t\tdst[dsti+1] = dbuf[1]\u003c\u003c6 | dbuf[2]\u003c\u003c1 | dbuf[3]\u003e\u003e4\n\t\t\tn++\n\t\t\tfallthrough\n\t\tcase 2:\n\t\t\tdst[dsti+0] = dbuf[0]\u003c\u003c3 | dbuf[1]\u003e\u003e2\n\t\t\tn++\n\t\t}\n\t\tdsti += 5\n\t}\n\treturn n, nil\n}\n\ntype encoder struct {\n\terr  error\n\tw    io.Writer\n\tenc  func(dst, src []byte)\n\tbuf  [5]byte    // buffered data waiting to be encoded\n\tnbuf int        // number of bytes in buf\n\tout  [1024]byte // output buffer\n}\n\nfunc NewEncoder(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: Encode}\n}\n\nfunc NewEncoderLower(w io.Writer) io.WriteCloser {\n\treturn \u0026encoder{w: w, enc: EncodeLower}\n}\n\nfunc (e *encoder) Write(p []byte) (n int, err error) {\n\tif e.err != nil {\n\t\treturn 0, e.err\n\t}\n\n\t// Leading fringe.\n\tif e.nbuf \u003e 0 {\n\t\tvar i int\n\t\tfor i = 0; i \u003c len(p) \u0026\u0026 e.nbuf \u003c 5; i++ {\n\t\t\te.buf[e.nbuf] = p[i]\n\t\t\te.nbuf++\n\t\t}\n\t\tn += i\n\t\tp = p[i:]\n\t\tif e.nbuf \u003c 5 {\n\t\t\treturn\n\t\t}\n\t\te.enc(e.out[0:], e.buf[0:])\n\t\tif _, e.err = e.w.Write(e.out[0:8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\te.nbuf = 0\n\t}\n\n\t// Large interior chunks.\n\tfor len(p) \u003e= 5 {\n\t\tnn := len(e.out) / 8 * 5\n\t\tif nn \u003e len(p) {\n\t\t\tnn = len(p)\n\t\t\tnn -= nn % 5\n\t\t}\n\t\te.enc(e.out[0:], p[0:nn])\n\t\tif _, e.err = e.w.Write(e.out[0 : nn/5*8]); e.err != nil {\n\t\t\treturn n, e.err\n\t\t}\n\t\tn += nn\n\t\tp = p[nn:]\n\t}\n\n\t// Trailing fringe.\n\tcopy(e.buf[:], p)\n\te.nbuf = len(p)\n\tn += len(p)\n\treturn\n}\n\n// Close flushes any pending output from the encoder.\n// It is an error to call Write after calling Close.\nfunc (e *encoder) Close() error {\n\t// If there's anything left in the buffer, flush it out\n\tif e.err == nil \u0026\u0026 e.nbuf \u003e 0 {\n\t\te.enc(e.out[0:], e.buf[0:e.nbuf])\n\t\tencodedLen := EncodedLen(e.nbuf)\n\t\te.nbuf = 0\n\t\t_, e.err = e.w.Write(e.out[0:encodedLen])\n\t}\n\treturn e.err\n}\n\n// Decode decodes src using cford32. It writes at most\n// [DecodedLen](len(src)) bytes to dst and returns the number of bytes\n// written. If src contains invalid cford32 data, it will return the\n// number of bytes successfully written and [CorruptInputError].\n// Newline characters (\\r and \\n) are ignored.\nfunc Decode(dst, src []byte) (n int, err error) {\n\tbuf := make([]byte, len(src))\n\tl := stripNewlines(buf, src)\n\treturn decode(dst, buf[:l])\n}\n\n// AppendDecode appends the cford32 decoded src to dst\n// and returns the extended buffer.\n// If the input is malformed, it returns the partially decoded src and an error.\nfunc AppendDecode(dst, src []byte) ([]byte, error) {\n\tn := DecodedLen(len(src))\n\n\tdst = grow(dst, n)\n\tdstsl := dst[len(dst) : len(dst)+n]\n\tn, err := Decode(dstsl, src)\n\treturn dst[:len(dst)+n], err\n}\n\n// DecodeString returns the bytes represented by the cford32 string s.\nfunc DecodeString(s string) ([]byte, error) {\n\tbuf := []byte(s)\n\tl := stripNewlines(buf, buf)\n\tn, err := decode(buf, buf[:l])\n\treturn buf[:n], err\n}\n\n// stripNewlines removes newline characters and returns the number\n// of non-newline characters copied to dst.\nfunc stripNewlines(dst, src []byte) int {\n\toffset := 0\n\tfor _, b := range src {\n\t\tif b == '\\r' || b == '\\n' {\n\t\t\tcontinue\n\t\t}\n\t\tdst[offset] = b\n\t\toffset++\n\t}\n\treturn offset\n}\n\ntype decoder struct {\n\terr    error\n\tr      io.Reader\n\tbuf    [1024]byte // leftover input\n\tnbuf   int\n\tout    []byte // leftover decoded output\n\toutbuf [1024 / 8 * 5]byte\n}\n\n// NewDecoder constructs a new base32 stream decoder.\nfunc NewDecoder(r io.Reader) io.Reader {\n\treturn \u0026decoder{r: \u0026newlineFilteringReader{r}}\n}\n\nfunc readEncodedData(r io.Reader, buf []byte) (n int, err error) {\n\tfor n \u003c 1 \u0026\u0026 err == nil {\n\t\tvar nn int\n\t\tnn, err = r.Read(buf[n:])\n\t\tn += nn\n\t}\n\treturn\n}\n\nfunc (d *decoder) Read(p []byte) (n int, err error) {\n\t// Use leftover decoded output from last read.\n\tif len(d.out) \u003e 0 {\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t\tif len(d.out) == 0 {\n\t\t\treturn n, d.err\n\t\t}\n\t\treturn n, nil\n\t}\n\n\tif d.err != nil {\n\t\treturn 0, d.err\n\t}\n\n\t// Read nn bytes from input, bounded [8,len(d.buf)]\n\tnn := (len(p)/5 + 1) * 8\n\tif nn \u003e len(d.buf) {\n\t\tnn = len(d.buf)\n\t}\n\n\tnn, d.err = readEncodedData(d.r, d.buf[d.nbuf:nn])\n\td.nbuf += nn\n\tif d.nbuf \u003c 1 {\n\t\treturn 0, d.err\n\t}\n\n\t// Decode chunk into p, or d.out and then p if p is too small.\n\tnr := d.nbuf\n\tif d.err != io.EOF \u0026\u0026 nr%8 != 0 {\n\t\tnr -= nr % 8\n\t}\n\tnw := DecodedLen(d.nbuf)\n\n\tif nw \u003e len(p) {\n\t\tnw, err = decode(d.outbuf[0:], d.buf[0:nr])\n\t\td.out = d.outbuf[0:nw]\n\t\tn = copy(p, d.out)\n\t\td.out = d.out[n:]\n\t} else {\n\t\tn, err = decode(p, d.buf[0:nr])\n\t}\n\td.nbuf -= nr\n\tfor i := 0; i \u003c d.nbuf; i++ {\n\t\td.buf[i] = d.buf[i+nr]\n\t}\n\n\tif err != nil \u0026\u0026 (d.err == nil || d.err == io.EOF) {\n\t\td.err = err\n\t}\n\n\tif len(d.out) \u003e 0 {\n\t\t// We cannot return all the decoded bytes to the caller in this\n\t\t// invocation of Read, so we return a nil error to ensure that Read\n\t\t// will be called again.  The error stored in d.err, if any, will be\n\t\t// returned with the last set of decoded bytes.\n\t\treturn n, nil\n\t}\n\n\treturn n, d.err\n}\n\ntype newlineFilteringReader struct {\n\twrapped io.Reader\n}\n\nfunc (r *newlineFilteringReader) Read(p []byte) (int, error) {\n\tn, err := r.wrapped.Read(p)\n\tfor n \u003e 0 {\n\t\ts := p[0:n]\n\t\toffset := stripNewlines(s, s)\n\t\tif err != nil || offset \u003e 0 {\n\t\t\treturn offset, err\n\t\t}\n\t\t// Previous buffer entirely whitespace, read again\n\t\tn, err = r.wrapped.Read(p)\n\t}\n\treturn n, err\n}\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package cford32 implements a modified base32 encoding based on Douglas\n// Crockford's base32 encoding.\npackage cford32\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/cford32/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"seqid","path":"gno.land/p/nt/seqid/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `seqid` - Sequential IDs\n\nSequential ID generator producing ordered binary and string representations suitable for use as AVL tree keys. String IDs use [cford32](../../cford32/v0)'s compact encoding and preserve lexicographic ordering.\n\n## Usage\n\n```go\nimport (\n    \"gno.land/p/nt/avl/v0\"\n    \"gno.land/p/nt/seqid/v0\"\n)\n\nvar (\n    id    seqid.ID\n    users avl.Tree\n)\n\nfunc NewUser(name string) {\n    user := \u0026User{Name: name}\n\n    // String() is human-friendly and preserves ordering.\n    users.Set(id.Next().String(), user)\n\n    // Or persist the binary form as a fixed-width 8-byte AVL key.\n    users.Set(id.Next().Binary(), user)\n}\n\n// Recover an ID from user input (case-insensitive, sanitized).\nfunc Lookup(raw string) (seqid.ID, error) {\n    return seqid.FromString(raw)\n}\n```\n\n## API\n\n```go\n// An ID is a sequential ID. The zero value is valid; the first\n// Next() call returns 1.\ntype ID uint64\n\n// Next advances the ID and returns the new value. Panics on overflow.\nfunc (i *ID) Next() ID\n\n// TryNext is like Next but returns false instead of panicking on overflow.\nfunc (i *ID) TryNext() (ID, bool)\n\n// Binary returns a fixed 8-byte big-endian encoding of the ID, suitable\n// as an AVL key. Lexicographic order matches numeric order.\nfunc (i ID) Binary() string\n\n// String returns the cford32 compact encoding of the ID: 7 bytes for\n// IDs in [0, 2^34), 13 bytes after that. Lexicographic order matches\n// numeric order across the rollover.\nfunc (i ID) String() string\n\n// FromBinary parses a value produced by Binary.\nfunc FromBinary(b string) (ID, bool)\n\n// FromString parses a cford32-encoded ID. Case-insensitive; maps\n// I/L to 1 and O to 0. Always re-encode user input via FromString\n// then String() before using it as a key.\nfunc FromString(b string) (ID, error)\n```\n\n## Notes\n\n- `Binary()` is the cheapest and most compact key (8 bytes, fixed width). Prefer it for internal storage. The keys work with any `ITree` (`gno.land/p/nt/avl/v0` or `gno.land/p/nt/bptree/v0`); their monotonic order suits bptree's append path especially well.\n- `String()` is human-friendly and URL-safe; use it for IDs surfaced to users.\n- Because cford32 accepts multiple spellings for the same value, always normalize external input through `FromString` then `String()` before using it as a lookup key.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package seqid provides a simple way to have sequential IDs which will be\n// ordered correctly when inserted in an AVL tree.\npackage seqid\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/seqid/v0\"\ngno = \"0.9\"\n"},{"name":"seqid.gno","body":"// Package seqid provides a simple way to have sequential IDs which will be\n// ordered correctly when inserted in an AVL tree.\n//\n// Sample usage:\n//\n//\tvar id seqid.ID\n//\tvar users avl.Tree\n//\n//\tfunc NewUser() {\n//\t\tusers.Set(id.Next().String(), \u0026User{ ... })\n//\t}\npackage seqid\n\nimport (\n\t\"encoding/binary\"\n\n\t\"gno.land/p/nt/cford32/v0\"\n)\n\n// An ID is a simple sequential ID generator.\ntype ID uint64\n\n// Next advances the ID i.\n// It will panic if increasing ID would overflow.\nfunc (i *ID) Next() ID {\n\tnext, ok := i.TryNext()\n\tif !ok {\n\t\tpanic(\"seqid: next ID overflows uint64\")\n\t}\n\treturn next\n}\n\nconst maxID ID = 1\u003c\u003c64 - 1\n\n// TryNext increases i by 1 and returns its value.\n// It returns true if successful, or false if the increment would result in\n// an overflow.\nfunc (i *ID) TryNext() (ID, bool) {\n\tif *i == maxID {\n\t\t// Addition will overflow.\n\t\treturn 0, false\n\t}\n\t*i++\n\treturn *i, true\n}\n\n// Binary returns a big-endian binary representation of the ID,\n// suitable to be used as an AVL key.\nfunc (i ID) Binary() string {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(buf, uint64(i))\n\treturn string(buf)\n}\n\n// String encodes i using cford32's compact encoding. For more information,\n// see the documentation for package [gno.land/p/nt/cford32/v0].\n//\n// The result of String will be a 7-byte string for IDs [0,2^34), and a\n// 13-byte string for all values following that. All generated string IDs\n// follow the same lexicographic order as their number values; that is, for any\n// two IDs (x, y) such that x \u003c y, x.String() \u003c y.String().\n// As such, this string representation is suitable to be used as an AVL key.\nfunc (i ID) String() string {\n\treturn string(cford32.PutCompact(uint64(i)))\n}\n\n// FromBinary creates a new ID from the given string, expected to be a binary\n// big-endian encoding of an ID (such as that of [ID.Binary]).\n// The second return value is true if the conversion was successful.\nfunc FromBinary(b string) (ID, bool) {\n\tif len(b) != 8 {\n\t\treturn 0, false\n\t}\n\treturn ID(binary.BigEndian.Uint64([]byte(b))), true\n}\n\n// FromString creates a new ID from the given string, expected to be a string\n// representation using cford32, such as that returned by [ID.String].\n//\n// The encoding scheme used by cford32 allows the same ID to have many\n// different representations (though the one returned by [ID.String] is only\n// one, deterministic and safe to be used in AVL). The encoding scheme is\n// \"human-centric\" and is thus case insensitive, and maps some ambiguous\n// characters to be the same, ie. L = I = 1, O = 0. For this reason, when\n// parsing user input to retrieve a key (encoded as a string), always sanitize\n// it first using FromString, then run String(), instead of using the user's\n// input directly.\nfunc FromString(b string) (ID, error) {\n\tn, err := cford32.Uint64([]byte(b))\n\treturn ID(n), err\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"dao","path":"gno.land/r/gov/dao","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"},{"name":"proxy.gno","body":"package dao\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// dao is the actual govDAO implementation, having all the needed business logic\nvar dao DAO\n\n// allowedDAOs contains realms that can be used to update the actual govDAO implementation,\n// and validate Proposals.\n// This is like that to be able to rollback using a previous govDAO implementation in case\n// the latest implementation has a breaking bug. After a test period, a proposal can be\n// executed to remove all previous govDAOs implementations and leave the last one.\nvar allowedDAOs []string\n\n// proposals contains all the proposals in history.\nvar proposals *Proposals = NewProposals()\n\n// Render calls directly to Render's DAO implementation.\n// This allows to have this realm as the main entry point for everything.\nfunc Render(cur realm, p string) string {\n\tif dao == nil {\n\t\treturn \"DAO not initialized\"\n\t}\n\treturn dao.Render(cross(cur), cur.PkgPath(), p)\n}\n\n// MustCreateProposal is an utility method that does the same as CreateProposal,\n// but instead of erroing if something happens, it panics.\nfunc MustCreateProposal(cur realm, r ProposalRequest) ProposalID {\n\tpid, err := CreateProposal(cur, r)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\treturn pid\n}\n\n// ExecuteProposal will try to execute the proposal with the provided ProposalID.\n// If the proposal was denied, it will return false. If the proposal is correctly\n// executed, it will return true. If something happens this function will panic.\nfunc ExecuteProposal(cur realm, pid ProposalID) bool {\n\treturn executeProposal(cur, pid, false)\n}\n\n// ExecuteOrRejectProposal executes the proposal with the provided ProposalID or rejects\n// it when there is an execution error.\n// If the proposal was denied, it will return false. If the proposal is correctly\n// executed, it will return true, unless execution fails with an error, in which case\n// proposal is rejected with the error as the reason.\n// This function allows to finish proposals by rejecting them when there is a state\n// change or an error in the proposal parameters that makes execution fail, potentially\n// leaving the proposal active forever because it can't be successfully executed.\nfunc ExecuteOrRejectProposal(cur realm, pid ProposalID) bool {\n\treturn executeProposal(cur, pid, true)\n}\n\n// CreateProposal will try to create a new proposal, that will be validated by the actual\n// govDAO implementation. If the proposal cannot be created, an error will be returned.\nfunc CreateProposal(cur realm, r ProposalRequest) (ProposalID, error) {\n\tif dao == nil {\n\t\treturn -1, errors.New(\"DAO not initialized\")\n\t}\n\tauthor, err := dao.PreCreateProposal(0, cur, r)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\tp := \u0026Proposal{\n\t\tauthor:      author,\n\t\ttitle:       r.title,\n\t\tdescription: r.description,\n\t\texecutor:    r.executor,\n\t\tallowedDAOs: allowedDAOs[:],\n\t}\n\n\tpid := proposals.SetProposal(p)\n\tdao.PostCreateProposal(0, cur, r, pid)\n\n\tchain.Emit(\"ProposalCreated\",\n\t\t\"id\", strconv.FormatInt(int64(pid), 10),\n\t)\n\n\treturn pid, nil\n}\n\nfunc MustVoteOnProposal(cur realm, r VoteRequest) {\n\tif err := VoteOnProposal(cur, r); err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\n// VoteOnProposal sends a vote to the actual govDAO implementation.\n// If the voter cannot vote the specified proposal, this method will return an error\n// with the explanation of why.\nfunc VoteOnProposal(cur realm, r VoteRequest) error {\n\tif dao == nil {\n\t\treturn errors.New(\"DAO not initialized\")\n\t}\n\treturn dao.VoteOnProposal(0, cur, r)\n}\n\n// MustVoteOnProposalSimple is like MustVoteOnProposal but intended to be used through gnokey with basic types.\nfunc MustVoteOnProposalSimple(cur realm, pid int64, option string) {\n\tMustVoteOnProposal(cur, VoteRequest{\n\t\tOption:     VoteOption(option),\n\t\tProposalID: ProposalID(pid),\n\t})\n}\n\nfunc MustGetProposal(pid ProposalID) *Proposal {\n\tp, err := GetProposal(pid)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\treturn p\n}\n\n// GetProposal gets created proposal by its ID. Non-crossing pure read:\n// looks up the proposal in this realm's package var. Callable directly\n// from any realm without cross-call syntax.\nfunc GetProposal(pid ProposalID) (*Proposal, error) {\n\tif dao == nil {\n\t\treturn nil, errors.New(\"DAO not initialized\")\n\t}\n\tprop := proposals.GetProposal(pid)\n\tif prop == nil {\n\t\treturn nil, errors.New(ufmt.Sprintf(\"Proposal %v does not exist.\", int64(pid)))\n\t}\n\treturn prop, nil\n}\n\n// UpdateImpl is a method intended to be used on a proposal.\n// This method will update the current govDAO implementation\n// to a new one. AllowedDAOs are a list of realms that can\n// call this method, in case the new DAO implementation had\n// a breaking bug. Any value set as nil will be ignored.\n// If AllowedDAOs field is not set correctly, the actual DAO\n// implementation wont be able to execute new Proposals!\nfunc UpdateImpl(cur realm, r UpdateRequest) {\n\tgRealm := cur.Previous().PkgPath()\n\n\tif !InAllowedDAOs(gRealm) {\n\t\tpanic(\"permission denied for prev realm: \" + gRealm)\n\t}\n\n\tif r.AllowedDAOs != nil {\n\t\tallowedDAOs = r.AllowedDAOs\n\t}\n\n\tif r.DAO != nil {\n\t\tdao = r.DAO\n\t}\n}\n\nfunc AllowedDAOs() []string {\n\tdup := make([]string, len(allowedDAOs))\n\tcopy(dup, allowedDAOs)\n\treturn dup\n}\n\nfunc InAllowedDAOs(pkg string) bool {\n\tif len(allowedDAOs) == 0 {\n\t\treturn true // corner case for initialization\n\t}\n\tfor _, d := range allowedDAOs {\n\t\tif pkg == d {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc executeProposal(cur realm, pid ProposalID, execErrorRejects bool) bool {\n\tif dao == nil {\n\t\treturn false\n\t}\n\texecute, err := dao.PreExecuteProposal(0, cur, pid)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tif !execute {\n\t\treturn false\n\t}\n\tprop, err := GetProposal(pid)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\terr = dao.ExecuteProposal(0, cur, pid, prop.executor)\n\tif err != nil {\n\t\tif execErrorRejects {\n\t\t\treturn false\n\t\t}\n\n\t\tpanic(err.Error())\n\t}\n\treturn true\n}\n"},{"name":"types.gno","body":"package dao\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\ntype ProposalID int64\n\nfunc (pid ProposalID) String() string {\n\treturn seqid.ID(pid).String()\n}\n\n// VoteOption is the limited voting option for a DAO proposal\n// New govDAOs can create their own VoteOptions if needed in the\n// future.\ntype VoteOption string\n\nconst (\n\tAbstainVote VoteOption = \"ABSTAIN\" // Side is not chosen\n\tYesVote     VoteOption = \"YES\"     // Proposal should be accepted\n\tNoVote      VoteOption = \"NO\"      // Proposal should be rejected\n)\n\ntype VoteRequest struct {\n\tOption     VoteOption\n\tProposalID ProposalID\n\tMetadata   interface{}\n}\n\nfunc NewVoteRequest(option VoteOption, proposalID ProposalID) VoteRequest {\n\treturn VoteRequest{\n\t\tOption:     option,\n\t\tProposalID: proposalID,\n\t}\n}\n\nfunc NewVoteRequestWithMetadata(option VoteOption, proposalID ProposalID, metadata interface{}) VoteRequest {\n\treturn VoteRequest{\n\t\tOption:     option,\n\t\tProposalID: proposalID,\n\t\tMetadata:   metadata,\n\t}\n}\n\nfunc NewProposalRequest(title string, description string, executor Executor) ProposalRequest {\n\treturn ProposalRequest{\n\t\ttitle:       title,\n\t\tdescription: description,\n\t\texecutor:    executor,\n\t}\n}\n\nfunc NewProposalRequestWithFilter(title string, description string, executor Executor, filter Filter) ProposalRequest {\n\treturn ProposalRequest{\n\t\ttitle:       title,\n\t\tdescription: description,\n\t\texecutor:    executor,\n\t\tfilter:      filter,\n\t}\n}\n\ntype Filter interface{}\n\ntype ProposalRequest struct {\n\ttitle       string\n\tdescription string\n\texecutor    Executor\n\tfilter      Filter\n}\n\nfunc (p *ProposalRequest) Title() string {\n\treturn p.title\n}\n\nfunc (p *ProposalRequest) Description() string {\n\treturn p.description\n}\n\nfunc (p *ProposalRequest) Filter() Filter {\n\treturn p.filter\n}\n\ntype Proposal struct {\n\tauthor address\n\n\ttitle       string\n\tdescription string\n\n\texecutor    Executor\n\tallowedDAOs []string\n}\n\nfunc (p *Proposal) Author() address {\n\treturn p.author\n}\n\nfunc (p *Proposal) Title() string {\n\treturn p.title\n}\n\nfunc (p *Proposal) Description() string {\n\treturn p.description\n}\n\nfunc (p *Proposal) ExecutorString() string {\n\tif p.executor != nil {\n\t\treturn p.executor.String()\n\t}\n\n\treturn \"\"\n}\n\nfunc (p *Proposal) ExecutorCreationRealm() string {\n\tif p.executor != nil {\n\t\treturn p.executor.CreationRealm()\n\t}\n\n\treturn \"\"\n}\n\nfunc (p *Proposal) AllowedDAOs() []string {\n\treturn append([]string(nil), p.allowedDAOs...)\n}\n\ntype Proposals struct {\n\tseq            seqid.ID\n\t*bptree.BPTree // *bptree.BPTree[ProposalID]*Proposal\n}\n\nfunc NewProposals() *Proposals {\n\treturn \u0026Proposals{BPTree: bptree.NewBPTree32()}\n}\n\nfunc (ps *Proposals) SetProposal(p *Proposal) ProposalID {\n\tpid := ProposalID(int64(ps.seq))\n\tupdated := ps.Set(pid.String(), p)\n\tif updated {\n\t\tpanic(\"fatal error: Override proposals is not allowed\")\n\t}\n\tps.seq = ps.seq.Next()\n\treturn pid\n}\n\nfunc (ps *Proposals) GetProposal(pid ProposalID) *Proposal {\n\tpv := ps.Get(pid.String())\n\tif pv == nil {\n\t\treturn nil\n\t}\n\n\treturn pv.(*Proposal)\n}\n\ntype Executor interface {\n\tExecute(cur realm) error\n\tString() string\n\tCreationRealm() string\n}\n\n// NewSimpleExecutor constructs an Executor whose creationRealm is captured\n// from rlm.PkgPath() at construction time. The IsCurrent() check rejects\n// stale or stashed realm values so the captured value is the authentic\n// caller realm. creationRealm is display-only (rendered as \"Executor\n// created in: ...\" in proposal listings) — no auth gate downstream.\nfunc NewSimpleExecutor(_ int, rlm realm, callback func(realm) error, description string) *SimpleExecutor {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"NewSimpleExecutor: rlm is not the caller's live cur (stale capture or sibling frame)\")\n\t}\n\tif callback == nil {\n\t\tpanic(\"executor callback must not be nil\")\n\t}\n\n\treturn \u0026SimpleExecutor{\n\t\tcallback:      callback,\n\t\tdesc:          description,\n\t\tcreationRealm: rlm.PkgPath(),\n\t}\n}\n\n// SimpleExecutor implements the Executor interface using\n// a callback function and a description string.\ntype SimpleExecutor struct {\n\tcallback      func(realm) error\n\tdesc          string\n\tcreationRealm string\n}\n\nfunc (e *SimpleExecutor) Execute(cur realm) error {\n\t// Check if executor was created using the constructor func\n\tif e.callback == nil {\n\t\treturn nil\n\t}\n\n\treturn e.callback(cross(cur))\n}\n\nfunc (e *SimpleExecutor) String() string {\n\treturn e.desc\n}\n\nfunc (e *SimpleExecutor) CreationRealm() string {\n\treturn e.creationRealm\n}\n\nfunc NewSafeExecutor(e Executor) *SafeExecutor {\n\treturn \u0026SafeExecutor{\n\t\te: e,\n\t}\n}\n\n// SafeExecutor wraps an Executor to only allow its execution\n// by allowed govDAOs.\ntype SafeExecutor struct {\n\te Executor\n}\n\nfunc (e *SafeExecutor) Execute(cur realm) error {\n\t// Verify the caller is an adequate Realm\n\tif !InAllowedDAOs(cur.Previous().PkgPath()) {\n\t\treturn errors.New(\"execution only allowed by validated govDAOs\")\n\t}\n\n\treturn e.e.Execute(cross(cur))\n}\n\nfunc (e *SafeExecutor) String() string {\n\treturn e.e.String()\n}\n\nfunc (e *SafeExecutor) CreationRealm() string {\n\treturn e.e.CreationRealm()\n}\n\n// DAO is the govDAO implementation interface. All mutating/auth-gated\n// methods take rlm as their realm-typed parameter in the second position\n// (the `_ int, rlm realm` non-crossing form): callers thread the proxy's\n// cur as data without forcing a realm transition, so the impl's existing\n// unsafe.CurrentRealm()-based auth gates (isValidCall, memberstore.Get)\n// continue to see the proxy realm. Render stays unchanged.\ntype DAO interface {\n\t// PreCreateProposal is called just before creating a new Proposal\n\t// It is intended to be used to get the address of the proposal, that\n\t// may vary depending on the DAO implementation, and to validate that\n\t// the requester is allowed to do a proposal\n\tPreCreateProposal(_ int, rlm realm, r ProposalRequest) (address, error)\n\n\t// PostCreateProposal is called after creating the Proposal. It is\n\t// intended to be used as a way to store a new proposal status, that\n\t// depends on the actuall govDAO implementation\n\tPostCreateProposal(_ int, rlm realm, r ProposalRequest, pid ProposalID)\n\n\t// VoteOnProposal will send a petition to vote for a specific proposal\n\t// to the actual govDAO implementation\n\tVoteOnProposal(_ int, rlm realm, r VoteRequest) error\n\n\t// PreExecuteProposal is called when someone is trying to execute a proposal by ID.\n\t// Is intended to be used to validate who can trigger the proposal execution.\n\tPreExecuteProposal(_ int, rlm realm, pid ProposalID) (bool, error)\n\n\t// ExecuteProposal executes the proposal executor and on error changes proposal\n\t// status to denied with the error message being the denial reason.\n\t// It returns the executor error when it fails.\n\tExecuteProposal(_ int, rlm realm, pid ProposalID, e Executor) error\n\n\t// Render will return a human-readable string in markdown format that\n\t// will be used to show new data through the dao proxy entrypoint.\n\t// Crossing: the chain query layer auto-injects .cur, and\n\t// implementations forward cur to internal rlm-aware helpers (mux\n\t// RenderRlm + downstream cross(rlm) reads).\n\tRender(cur realm, pkgpath string, path string) string\n}\n\ntype UpdateRequest struct {\n\tDAO         DAO\n\tAllowedDAOs []string\n}\n\n// NewUpdateRequest copies allowedDAOs into a fresh slice owned by\n// /r/gov/dao. Under the storage=authority model, if we stored the\n// caller-passed slice directly, the base ArrayValue would retain\n// PkgID = caller_realm: storage rent would attribute to caller, and\n// /r/gov/dao could not mutate (e.g. append to) its own copy without\n// a DidUpdate panic. The internal copy ensures the UpdateRequest\n// and its AllowedDAOs both live entirely in /r/gov/dao's authority.\nfunc NewUpdateRequest(d DAO, allowedDAOs []string) UpdateRequest {\n\tcp := make([]string, len(allowedDAOs))\n\tcopy(cp, allowedDAOs)\n\treturn UpdateRequest{\n\t\tDAO:         d,\n\t\tAllowedDAOs: cp,\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"users","path":"gno.land/r/sys/users","files":[{"name":"README.md","body":"# `r/sys/users`\n\nThe system realm that owns the (name → address, address → user) registry for\ngno.land. It is intentionally minimal: it stores `UserData` records, exposes\nresolve/update/delete primitives, and gates writes through a controller\nwhitelist managed by GovDAO (`ProposeNewController` /\n`ProposeControllerRemoval` / `ProposeControllerAdditionAndRemoval`).\n\nThis realm does **not** define what a \"name\" is, what registration costs, or\nwhether names can be transferred. Those policies live in *controller realms*\nthat the DAO whitelists. See `r/sys/namereg/v1` for one such controller, and\nthe `examples/gno.land/r/sys/names` realm for the related namespace verifier\nthat gates package deployment under `gno.land/r/\u003cnamespace\u003e/...`.\n\n## Trust boundary at genesis (height 0)\n\nThe whitelist check in `RegisterUser` (and the sibling\n`AddControllerAtGenesis`) **short-circuits at chain height 0**:\n\n```go\n// store.gno\nif runtime.ChainHeight() \u003e 0 \u0026\u0026 !controllers.Has(runtime.PreviousRealm().Address()) {\n    return NewErrNotWhitelisted()\n}\n```\n\nThis is **intentional**, not a bug. Genesis is the bootstrap window where:\n\n1. The controller whitelist is empty (it can't be populated until *after* it\n   exists).\n2. System realms (`r/sys/users/init`, `r/sys/namereg/v1`, etc.) need to\n   pre-seed users and add themselves as controllers.\n3. Any realm whose `init()` runs at genesis can therefore call `RegisterUser`\n   without authorization.\n\nThe protection model is **out-of-band trust**: chain operators control which\nrealms ship in genesis (via the contents of `examples/gno.land/r/...`), and\nthose realms are vouched for at chain-binary build time. The realm code does\nnot — and intentionally does not try to — enforce who is \"allowed\" to\npre-register at height 0.\n\n### Audit reference\n\nThis bypass was flagged as audit finding #4 (\"Genesis bypass — any caller can\nregister at height 0\"). After review, it is treated as **WON'T FIX, working\nas intended**:\n\n- Removing the bypass breaks every legitimate genesis pre-registration use\n  case (including this realm's own bootstrap and `r/sys/namereg/v1`'s\n  preregister loop of system names).\n- A hardcoded genesis-allowlist (a la \"only `r/sys/*` realms may bypass\")\n  shifts the trust to a literal in source — a chain upgrade is required to\n  add a new genesis-bootstrap realm. This trades flexibility for the same\n  amount of trust.\n- Path-prefix gating (e.g. \"only `gno.land/r/sys/*`\") couples this realm to\n  the namespace verifier remaining locked-down, an implicit dependency that\n  makes future refactors fragile.\n\nIf chain operators want post-deployment auditing of who pre-registered what\nat genesis, the `RegisterUserEvent` is emitted on every successful\nregistration regardless of height, and the source of each registration can be\nrecovered by walking genesis-block events alongside the `examples/` tree.\n\n### Sibling bypass: `AddControllerAtGenesis`\n\nThe same height-0 trust model applies to `AddControllerAtGenesis` in\n`admin.gno`:\n\n```go\nfunc AddControllerAtGenesis(_ realm, addr address) {\n    height := runtime.ChainHeight()\n    if height \u003e 0 {\n        panic(\"AddControllerAtGenesis can only be called at genesis (height 0)\")\n    }\n    if !addr.IsValid() {\n        panic(ErrInvalidAddress)\n    }\n    controllers.Add(addr)\n}\n```\n\nThis was audit finding #7 (\"AddControllerAtGenesis has no caller check\"). It\nis the **same intentional design** as #4 and is likewise treated as **WON'T\nFIX**:\n\n- Any realm whose `init()` runs at genesis can whitelist any address as a\n  controller, without authorization.\n- This is how the registry bootstraps itself: `r/sys/users/init.Bootstrap`\n  adds its own package address, and `r/sys/namereg/v1/init.gno` likewise\n  auto-whitelists `gno.land/r/sys/namereg/v1`. Removing the bypass would\n  break the bootstrap pattern.\n- After genesis (height \u003e 0) the function hard-panics, so the privilege\n  window is strictly one-time at chain birth.\n- The trust model is identical: chain operators vouch for whatever realms\n  ship in `examples/` at chain-binary build time.\n\nIf you need to add a new controller post-genesis, the supported path is a\nGovDAO proposal via `ProposeNewController` — the same channel that rotates\nevery controller going forward.\n\n### What the audit DID flag that's worth fixing\n\n- #5: `ufmt.Sprint` used instead of `Sprintf` in controller-swap proposal\n  description (governance-vote readability).\n- #6: Add+Remove proposal silently no-ops if `add` fails on an already-listed\n  controller — voted-on swap doesn't actually swap.\n- #7: `AddControllerAtGenesis` shares the height-0 bypass; same trust model\n  applies, same intentional design.\n\nSee `NAMEREG_AUDIT.md` for the full set and `NAMEREG_TODO.md` for tracked\n\"won't fix / accepted risk\" items.\n"},{"name":"admin.gno","body":"package users\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\nconst initControllerPath = \"gno.land/r/sys/users/init\"\n\nvar controllers = addrset.Set{} // caller whitelist\n\nfunc init() {\n\t// auto-whitelist the init controller for bootstrapping for testing chain.\n\tif chainID := runtime.ChainID(); chainID == \"dev\" {\n\t\tcontrollers.Add(chain.PackageAddress(initControllerPath))\n\t}\n}\n\n// AddControllerAtGenesis allows adding a controller during chain genesis (height 0).\n// This is mostly useful for testing.\nfunc AddControllerAtGenesis(_ realm, addr address) {\n\theight := runtime.ChainHeight()\n\tif height \u003e 0 {\n\t\tpanic(\"AddControllerAtGenesis can only be called at genesis (height 0)\")\n\t}\n\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tcontrollers.Add(addr)\n}\n\n// ProposeNewController allows GovDAO to add a whitelisted caller\nfunc ProposeNewController(cur realm, addr address) dao.ProposalRequest {\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn addToWhitelist(addr)\n\t}\n\n\tdesc := \"This proposal adds \" + addr.String() + \" to `sys/users` realm's callers whitelist.\"\n\treturn dao.NewProposalRequest(\"Add Whitelisted Caller to \\\"sys/users\\\" Realm\", desc, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// ProposeControllerRemoval allows GovDAO to add a whitelisted caller\nfunc ProposeControllerRemoval(cur realm, addr address) dao.ProposalRequest {\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn deleteFromWhitelist(addr)\n\t}\n\n\tdesc := \"This proposal removes \" + addr.String() + \" from `sys/users` realm's callers whitelist.\"\n\treturn dao.NewProposalRequest(\"Remove Whitelisted Caller From \\\"sys/users\\\" Realm\", desc, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// ProposeControllerAdditionAndRemoval allows GovDAO to add a new caller and remove an old caller in the same proposal.\nfunc ProposeControllerAdditionAndRemoval(cur realm, toAdd, toRemove address) dao.ProposalRequest {\n\tif !toAdd.IsValid() || !toRemove.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn applyControllerSwap(toAdd, toRemove)\n\t}\n\n\tdesc := ufmt.Sprintf(\n\t\t\"This proposal adds %s and removes %s from `sys/users` realm's callers whitelist.\",\n\t\ttoAdd,\n\t\ttoRemove,\n\t)\n\treturn dao.NewProposalRequest(\"Add and Remove Whitelisted Callers From \\\"sys/users\\\" Realm\", desc, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// applyControllerSwap is the callback body of ProposeControllerAdditionAndRemoval,\n// extracted so it can be unit-tested without driving the full GovDAO flow.\n//\n// The desired end state is \"toAdd is in the whitelist AND toRemove is out\".\n// Both operations are made idempotent so the swap doesn't silently no-op when\n// the chain state has drifted between proposal creation and execution:\n//\n//   - If toAdd is already whitelisted, treat addToWhitelist's\n//     ErrAlreadyWhitelisted as benign and continue to the remove step.\n//   - If toRemove is already absent, treat deleteFromWhitelist's\n//     ErrNotWhitelisted as benign and return success.\n//\n// Without this idempotency, the original code returned early on an \"already\n// whitelisted\" toAdd and skipped the remove entirely — a swap proposal could\n// pass governance and silently leave the old controller active. (audit\n// finding #6)\nfunc applyControllerSwap(toAdd, toRemove address) error {\n\tif err := addToWhitelist(toAdd); err != nil \u0026\u0026 err != ErrAlreadyWhitelisted {\n\t\treturn err\n\t}\n\tif err := deleteFromWhitelist(toRemove); err != nil {\n\t\tif _, alreadyOut := err.(ErrNotWhitelisted); !alreadyOut {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// ProposeRegisterUser allows GovDAO to register a name without checking\n// controllers. The executor closure runs with ignoreCanonical=true (decision\n// #3): DAO grants always bypass canonical-collision detection. Voters see\n// any collision in the proposal description and can vote NO if unintended.\nfunc ProposeRegisterUser(cur realm, name string, addr address) dao.ProposalRequest {\n\t// Validate the name and address now, even though registerUser will validate again\n\tif err := validateName(name); err != nil {\n\t\tpanic(err.Error())\n\t}\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tdesc := \"This proposal registers \" + name + \" with address \" + addr.String() + \" in `sys/users`.\"\n\tif existing, taken := IsCanonicalTaken(name); taken \u0026\u0026 existing != name {\n\t\tdesc += \"\\n\\nCANONICAL COLLISION: this name's canonical form matches the existing registration of `\" +\n\t\t\texisting + \"`. DAO grants bypass canonical-collision detection — the proposal will succeed if voted in. \" +\n\t\t\t\"If the collision is unintended, vote NO.\"\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn registerUser(cur, name, addr, true) // bypass canonical (decision #3)\n\t}\n\n\treturn dao.NewProposalRequest(\"Register User to \\\"sys/users\\\" Realm\", desc, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// ProposeUpdateName allows GovDAO to update a name with an alias without\n// checking controllers. Like ProposeRegisterUser, the executor runs with\n// ignoreCanonical=true (decision #3).\nfunc ProposeUpdateName(cur realm, addr address, newName string) dao.ProposalRequest {\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\tif err := validateName(newName); err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\tdesc := \"This proposal updates address \" + addr.String() + \" with alias \" + newName + \" in `sys/users`.\"\n\tif existing, taken := IsCanonicalTaken(newName); taken \u0026\u0026 existing != newName {\n\t\tdesc += \"\\n\\nCANONICAL COLLISION: the new alias's canonical form matches the existing registration of `\" +\n\t\t\texisting + \"`. DAO grants bypass canonical-collision detection — the proposal will succeed if voted in. \" +\n\t\t\t\"If the collision is unintended, vote NO.\"\n\t}\n\n\tcb := func(cur realm) error {\n\t\tdata := ResolveAddress(addr)\n\t\tif data == nil {\n\t\t\treturn ErrUserNotExistOrDeleted\n\t\t}\n\t\treturn data.updateName(newName, true) // bypass canonical (decision #3)\n\t}\n\n\treturn dao.NewProposalRequest(\"Update Name Alias in \\\"sys/users\\\" Realm\", desc, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// ProposeDeleteUser allows GovDAO to delete a user without checking controllers\nfunc ProposeDeleteUser(cur realm, addr address) dao.ProposalRequest {\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tcb := func(cur realm) error {\n\t\tdata := ResolveAddress(addr)\n\t\tif data == nil {\n\t\t\treturn ErrUserNotExistOrDeleted\n\t\t}\n\t\treturn data.delete()\n\t}\n\n\tdesc := \"This proposal deletes the user with address \" + addr.String() + \" in `sys/users`.\"\n\treturn dao.NewProposalRequest(\"Delete User in \\\"sys/users\\\" Realm\", desc, dao.NewSimpleExecutor(0, cur, cb, \"\"))\n}\n\n// IsController reports whether the given address is currently in the\n// controller whitelist. Returns the same boolean that gating checks\n// (RegisterUser, UpdateName, Delete) use internally — useful for\n// off-chain monitoring and for governance proposals to inspect state\n// before voting.\nfunc IsController(addr address) bool {\n\treturn controllers.Has(addr)\n}\n\n// Controllers returns a snapshot of the current controller whitelist.\n// The returned slice is a fresh copy; mutating it does not affect realm\n// state. Order is the iteration order of the underlying address set.\n//\n// Audit finding #20: without this getter, the controller whitelist was\n// opaque from outside the package — operators had to read source or\n// replay every governance proposal to know who could write to the\n// registry. This is the read-only API that closes that gap.\nfunc Controllers() []address {\n\tout := make([]address, 0, controllers.Size())\n\tcontrollers.IterateByOffset(0, controllers.Size(), func(a address) bool {\n\t\tout = append(out, a)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// Helpers\n\nfunc deleteFromWhitelist(addr address) error {\n\tif !controllers.Has(addr) {\n\t\treturn ErrNotWhitelisted{Caller: \"UserRealm{ \" + addr.String() + \" }\"}\n\t}\n\n\tif ok := controllers.Remove(addr); !ok {\n\t\treturn ErrWhitelistRemoveFailed\n\t}\n\n\treturn nil\n}\n\nfunc addToWhitelist(newCaller address) error {\n\tif !controllers.Add(newCaller) {\n\t\treturn ErrAlreadyWhitelisted\n\t}\n\n\treturn nil\n}\n"},{"name":"api.gno","body":"package users\n\n// IsNameTaken reports whether the exact-string name exists in nameStore.\n// Returns true for any name ever registered, including:\n//\n//   - active registrations\n//   - tombstoned (deleted) users' names — Delete() sets `deleted=true`\n//     but does not remove the nameStore entry (anti-revival policy)\n//   - old aliases from renames — UpdateName inserts the new name\n//     alongside the old; the old key stays (anti-rename-squat policy)\n//\n// In short: IsNameTaken(name) answers \"would RegisterUser(name, _)\n// fail with ErrNameTaken?\" — same answer for active, deleted, or\n// aliased-away names. Pairs with IsCanonicalTaken (canonical-match)\n// and ResolveName (active-current-user lookup with full UserData).\n//\n// No canonicalization is applied. For controllers that want exact-\n// match uniqueness without pulling in canonical-collision logic.\nfunc IsNameTaken(name string) bool {\n\treturn nameStore.Has(name)\n}\n\n// IsCanonicalTaken reports whether the given name's canonical form is\n// already registered. Pass the raw name; canonicalization is applied\n// internally. The first return is the original (non-canonical) name\n// that owns the canonical key, for UX in collision messages.\n//\n// When a bypass write (RegisterUserIgnoreCanonical or the bypass path\n// through ProposeRegisterUser/ProposeUpdateName) overwrites a prior\n// canonical entry, this returns the most-recently-written original.\nfunc IsCanonicalTaken(name string) (existing string, taken bool) {\n\tv := canonicalStore.Get(Canonicalize(name))\n\tif v == nil {\n\t\treturn \"\", false\n\t}\n\treturn v.(string), true\n}\n"},{"name":"canonical.gno","body":"package users\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\n// canonicalStore maps canonicalized full names to the original name\n// that was registered. Keyed by the result of Canonicalize.\n//\n// Multiple controllers (namereg/v1, future registries, governance,\n// genesis bootstrapping) all share this single store via RegisterUser\n// and the bypass variant RegisterUserIgnoreCanonical. Cross-controller\n// canonical-collision detection is uniform and atomic with the\n// nameStore write.\nvar canonicalStore = bptree.NewBPTree32()\n\n// Canonicalize returns the canonical form of a name. The substitutions\n// collapse single-character visual confusables that arise across the\n// allowed [a-z0-9] character set, and strip the three separators that\n// can sneak between identical alphanumeric runs.\n//\n//   - {l, i, 1} → i\n//   - {0, o}    → o\n//   - {-, ., _} → stripped\n//   - all other characters unchanged\n//\n// CONTRACT: stable. Future controllers that want to share this\n// canonical namespace MUST use this exact function — do not roll your\n// own. Adding new substitutions later is a breaking change because it\n// would silently re-key existing entries in canonicalStore.\n//\n// Input contract: ASCII-only. r/sys/users.validateName already rejects\n// non-ASCII at the registration boundary, so by the time a name reaches\n// Canonicalize through the standard write path it is guaranteed to be\n// ASCII. Direct callers from other realms must honor this contract;\n// non-ASCII bytes are passed through as-is and will produce undefined\n// collision behavior.\n//\n// Multi-char confusables (m↔rn, nn↔m, cl↔d) are NOT canonicalized.\n// They require fixed-point substring substitution rounds, which is out\n// of scope for the unified store.\n//\n// Pure: no state access. Safe to call from anywhere.\nfunc Canonicalize(name string) string {\n\tvar b strings.Builder\n\tb.Grow(len(name))\n\tfor i := 0; i \u003c len(name); i++ {\n\t\tc := name[i]\n\t\tswitch c {\n\t\tcase 'l', '1':\n\t\t\tb.WriteByte('i')\n\t\tcase '0':\n\t\t\tb.WriteByte('o')\n\t\tcase '-', '.', '_':\n\t\t\t// strip\n\t\tdefault:\n\t\t\tb.WriteByte(c)\n\t\t}\n\t}\n\treturn b.String()\n}\n"},{"name":"errors.gno","body":"package users\n\nimport (\n\t\"errors\"\n)\n\nconst prefix = \"r/sys/users: \"\n\nvar (\n\tErrAlreadyWhitelisted    = errors.New(prefix + \"already whitelisted\")\n\tErrWhitelistRemoveFailed = errors.New(prefix + \"failed to remove address from whitelist\")\n\n\tErrNameTaken          = errors.New(prefix + \"name/Alias already taken\")\n\tErrCanonicalCollision = errors.New(prefix + \"name collides with a confusable variant of an existing name\")\n\tErrInvalidAddress     = errors.New(prefix + \"invalid address\")\n\n\tErrEmptyUsername   = errors.New(prefix + \"empty username provided\")\n\tErrNameLikeAddress = errors.New(prefix + \"username resembles a gno.land address\")\n\tErrInvalidUsername = errors.New(prefix + \"username must match ^[a-z][a-z0-9]*([_-][a-z0-9]+)*$ (max 64 chars)\")\n\n\tErrAlreadyHasName = errors.New(prefix + \"username for this address already registered - try creating an Alias\")\n\tErrDeletedUser    = errors.New(prefix + \"cannot register a new username after deleting\")\n\n\tErrUserNotExistOrDeleted = errors.New(prefix + \"this user does not exist or was deleted\")\n\n\t// ErrInvalidRealm is returned by controller-gated *UserData mutators\n\t// when the supplied rlm is not the caller's live cur (i.e.\n\t// rlm.IsCurrent() is false). Closes Class-2 designation forgery via\n\t// a stored stale realm value whose .Address() resolves to a\n\t// whitelisted controller. See docs/resources/gno-security.md.\n\tErrInvalidRealm = errors.New(prefix + \"rlm is not the caller's live cur\")\n)\n\n// ErrNotWhitelisted stores the failing caller's realm identity as a\n// plain string so the error is a pure data record (no live realm values\n// in its fields).\ntype ErrNotWhitelisted struct {\n\tCaller string // \"CodeRealm{ \u003caddr\u003e, \u003cpkgPath\u003e }\" or \"UserRealm{ \u003caddr\u003e }\" — failed the whitelist check\n}\n\n// NewErrNotWhitelisted constructs the error with the caller's realm\n// identity captured as a string at construction time. The _ int\n// discriminator keeps this non-crossing (a non-crossing function can't\n// take a `realm`-named-`cur` first param, so we use the standard\n// _ int, rlm realm shape).\nfunc NewErrNotWhitelisted(_ int, caller realm) ErrNotWhitelisted {\n\treturn ErrNotWhitelisted{\n\t\tCaller: caller.String(),\n\t}\n}\n\nfunc (e ErrNotWhitelisted) Error() string {\n\treturn prefix + \"caller realm/user does not exist in whitelist: \" + e.Caller\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/users\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"render.gno","body":"package users\n\nimport \"gno.land/p/nt/ufmt/v0\"\n\nfunc Render(_ string) string {\n\tout := \"# r/sys/users\\n\\n\"\n\n\tout += \"`r/sys/users` is a system realm for managing user registrations.\\n\\n\"\n\tout += \"User registration is managed through whitelisted controller realms.\\n\\n\"\n\tout += \"---\\n\\n\"\n\n\tout += \"## Stats\\n\\n\"\n\tout += ufmt.Sprintf(\"Total unique addresses registered: **%d**\\n\\n\", addressStore.Size())\n\tout += ufmt.Sprintf(\"Total unique names registered: **%d**\\n\\n\", nameStore.Size())\n\treturn out\n}\n"},{"name":"store.gno","body":"package users\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"regexp\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tnameStore    = bptree.NewBPTree32() // name/aliases \u003e *UserData\n\taddressStore = bptree.NewBPTree32() // address \u003e *UserData\n\n\treAddressLookalike = regexp.MustCompile(`^g1[a-z0-9]{20,38}$`)\n\n\t// reName mirrors gno's package-name shape (gnovm/pkg/gnolang/mempackage.go\n\t// `Re_name`): start with a lowercase letter, optional alphanumeric body,\n\t// then any number of (separator + alphanumeric run) — so single hyphens\n\t// or underscores are allowed BETWEEN alphanumerics, but consecutive\n\t// separators (`--`, `__`, `-_`, `_-`) are rejected, and so are leading\n\t// or trailing separators. Lowercase-only — closes the case-confusable\n\t// squatting concern (Alice vs alice were two distinct names under the\n\t// previous case-preserving regex). Length cap of 64 enforced separately\n\t// in validateName.\n\treName = regexp.MustCompile(`^[a-z][a-z0-9]*([_-][a-z0-9]+)*$`)\n)\n\nconst maxNameLen = 64\n\nconst (\n\tRegisterUserEvent = \"Registered\"\n\tUpdateNameEvent   = \"Updated\"\n\tDeleteUserEvent   = \"Deleted\"\n)\n\ntype UserData struct {\n\taddr     address\n\tusername string // contains the latest name of a user\n\tdeleted  bool\n}\n\nfunc (u UserData) Name() string {\n\treturn u.username\n}\n\nfunc (u UserData) Addr() address {\n\treturn u.addr\n}\n\n// IsDeleted reports whether this user record is missing or marked deleted.\n// A nil receiver returns true — \"the user does not exist\" is semantically\n// indistinguishable from \"the user was deleted\" for callers that need to\n// gate further state changes. This lets call sites collapse the nil check\n// and the deleted check into a single guard:\n//\n//\tif u.IsDeleted() {\n//\t    return ErrUserNotExistOrDeleted\n//\t}\nfunc (u *UserData) IsDeleted() bool {\n\tif u == nil {\n\t\treturn true\n\t}\n\treturn u.deleted\n}\n\n// RenderLink provides a render link to the user page on gnoweb\n// `linkText` is optional\nfunc (u UserData) RenderLink(linkText string) string {\n\tif linkText == \"\" {\n\t\treturn ufmt.Sprintf(\"[@%s](/u/%s)\", u.username, u.username)\n\t}\n\n\treturn ufmt.Sprintf(\"[%s](/u/%s)\", linkText, u.username)\n}\n\n// registerUser adds a new user to the system without checking controllers.\n// The ignoreCanonical flag suppresses ErrCanonicalCollision; the canonical\n// store is written either way (decision #14: later-wins on bypass).\nfunc registerUser(cur realm, name string, address_XXX address, ignoreCanonical bool) error {\n\t// Validate name\n\tif err := validateName(name); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate address\n\tif !address_XXX.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\t// Check if name is taken (exact-string match precedes canonical check)\n\tif nameStore.Has(name) {\n\t\treturn ErrNameTaken\n\t}\n\n\tcanonical := Canonicalize(name)\n\tif !ignoreCanonical {\n\t\tif canonicalStore.Has(canonical) {\n\t\t\treturn ErrCanonicalCollision\n\t\t}\n\t}\n\n\traw := addressStore.Get(address_XXX.String())\n\tif raw != nil {\n\t\t// Cannot re-register after deletion\n\t\tif raw.(*UserData).IsDeleted() {\n\t\t\treturn ErrDeletedUser\n\t\t}\n\n\t\t// For a second name, use UpdateName\n\t\treturn ErrAlreadyHasName\n\t}\n\n\t// Create UserData\n\tdata := \u0026UserData{\n\t\taddr:     address_XXX,\n\t\tusername: name,\n\t\tdeleted:  false,\n\t}\n\n\t// Set corresponding stores\n\tnameStore.Set(name, data)\n\taddressStore.Set(address_XXX.String(), data)\n\tcanonicalStore.Set(canonical, name)\n\n\tchain.Emit(RegisterUserEvent,\n\t\t\"name\", name,\n\t\t\"address\", address_XXX.String(),\n\t)\n\treturn nil\n}\n\n// RegisterUser adds a new user to the system. Enforces canonical-\n// collision detection: a name whose Canonicalize-form matches a prior\n// registration returns ErrCanonicalCollision.\nfunc RegisterUser(cur realm, name string, address_XXX address) error {\n\t// At genesis (height 0), allow any caller to register users.\n\t// After genesis, only whitelisted controllers can register.\n\tif runtime.ChainHeight() \u003e 0 \u0026\u0026 !controllers.Has(cur.Previous().Address()) {\n\t\treturn NewErrNotWhitelisted(0, cur.Previous())\n\t}\n\n\treturn registerUser(cur, name, address_XXX, false)\n}\n\n// RegisterUserIgnoreCanonical is the bypass path: same controller-\n// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical\n// store is still written; a prior entry with the same canonical key is\n// silently overwritten (decision #14, later-wins). Use sparingly — names\n// registered here can canonical-collide with existing ones, weakening\n// confusable protection for everyone.\nfunc RegisterUserIgnoreCanonical(cur realm, name string, address_XXX address) error {\n\tif runtime.ChainHeight() \u003e 0 \u0026\u0026 !controllers.Has(cur.Previous().Address()) {\n\t\treturn NewErrNotWhitelisted(0, cur.Previous())\n\t}\n\n\treturn registerUser(cur, name, address_XXX, true)\n}\n\n// updateName adds a name that is associated with a specific address without\n// checking controllers. The ignoreCanonical flag suppresses\n// ErrCanonicalCollision; the canonical store is written either way (decision\n// #14: later-wins on bypass).\n//\n// All previous names are preserved and resolvable.\n// The new name is the default value returned for address lookups.\nfunc (u *UserData) updateName(newName string, ignoreCanonical bool) error {\n\t// IsDeleted handles both branches: nil receiver (user never existed)\n\t// AND a non-nil receiver whose .deleted is true (a controller cached\n\t// the *UserData pointer before the user was deleted by a separate\n\t// controller or governance proposal). Without the deleted-flag branch,\n\t// nameStore.Set(newName, u) would insert an alias pointing at a\n\t// deleted user — Has(newName) returns true forever but Resolve(newName)\n\t// returns nil (Resolve* APIs filter deleted), so the name is squatted\n\t// with no recovery path. (audit finding #3)\n\tif u.IsDeleted() {\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\t// Validate name\n\tif err := validateName(newName); err != nil {\n\t\treturn err\n\t}\n\n\t// Check if the requested Alias is already taken (exact-string match)\n\tif nameStore.Has(newName) {\n\t\treturn ErrNameTaken\n\t}\n\n\tcanonical := Canonicalize(newName)\n\tif !ignoreCanonical {\n\t\t// No self-collision filter (decision #15): even the user's OWN\n\t\t// prior canonical claim blocks the rename. Prevents accumulating\n\t\t// confusable aliases of one's own name through free renames. The\n\t\t// only path to a self-confusable rename is DAO governance via\n\t\t// ProposeUpdateName.\n\t\tif canonicalStore.Has(canonical) {\n\t\t\treturn ErrCanonicalCollision\n\t\t}\n\t}\n\n\tu.username = newName\n\tnameStore.Set(newName, u)\n\tcanonicalStore.Set(canonical, newName)\n\n\tchain.Emit(UpdateNameEvent,\n\t\t\"alias\", newName,\n\t\t\"address\", u.addr.String(),\n\t)\n\treturn nil\n}\n\n// UpdateName adds a name that is associated with a specific address.\n// Enforces canonical-collision detection.\n// All previous names are preserved and resolvable.\n// The new name is the default value returned for address lookups.\n//\n// rlm is the cur of the caller's enclosing crossing function (passed as\n// data via the `_ int, rlm realm` non-crossing form). rlm.Address() is\n// the calling realm against which we authorize.\nfunc (u *UserData) UpdateName(_ int, rlm realm, newName string) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrInvalidRealm\n\t}\n\tif u.IsDeleted() {\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\t// Validate caller\n\tif !controllers.Has(rlm.Address()) {\n\t\treturn NewErrNotWhitelisted(0, rlm)\n\t}\n\n\treturn u.updateName(newName, false)\n}\n\n// UpdateNameIgnoreCanonical is the bypass path: same controller-\n// whitelist gate, but ErrCanonicalCollision is suppressed. The canonical\n// store is still written; a prior entry with the same canonical key is\n// silently overwritten (decision #14, later-wins).\nfunc (u *UserData) UpdateNameIgnoreCanonical(_ int, rlm realm, newName string) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrInvalidRealm\n\t}\n\tif u.IsDeleted() {\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\tif !controllers.Has(rlm.Address()) {\n\t\treturn NewErrNotWhitelisted(0, rlm)\n\t}\n\n\treturn u.updateName(newName, true)\n}\n\n// delete marks a user and all their aliases as deleted without checking controllers.\nfunc (u *UserData) delete() error {\n\tif u.IsDeleted() {\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\tu.deleted = true\n\n\tchain.Emit(DeleteUserEvent, \"address\", u.addr.String())\n\treturn nil\n}\n\n// Delete marks a user and all their aliases as deleted.\n// rlm is the cur of the caller's enclosing crossing function; see UpdateName.\nfunc (u *UserData) Delete(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrInvalidRealm\n\t}\n\tif u.IsDeleted() {\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\t// Validate caller\n\tif !controllers.Has(rlm.Address()) {\n\t\treturn NewErrNotWhitelisted(0, rlm)\n\t}\n\n\treturn u.delete()\n}\n\n// Validate validates username and address passed in\n// Most of the validation is done in the controllers\n// This provides more flexibility down the line\nfunc validateName(username string) error {\n\tif username == \"\" {\n\t\treturn ErrEmptyUsername\n\t}\n\n\tif len(username) \u003e maxNameLen {\n\t\treturn ErrInvalidUsername\n\t}\n\n\tif !reName.MatchString(username) {\n\t\treturn ErrInvalidUsername\n\t}\n\n\t// Check if the username can be decoded or looks like a valid address\n\tif address(username).IsValid() || reAddressLookalike.MatchString(username) {\n\t\treturn ErrNameLikeAddress\n\t}\n\n\treturn nil\n}\n"},{"name":"users.gno","body":"package users\n\nimport \"gno.land/p/nt/bptree/v0/rotree\"\n\n// ResolveName returns the latest UserData of a specific user by name or alias\nfunc ResolveName(name string) (data *UserData, isCurrent bool) {\n\traw := nameStore.Get(name)\n\tif raw == nil {\n\t\treturn nil, false\n\t}\n\n\tdata = raw.(*UserData)\n\tif data.deleted {\n\t\treturn nil, false\n\t}\n\n\treturn data, name == data.username\n}\n\n// ResolveAddress returns the latest UserData of a specific user by address\nfunc ResolveAddress(addr address) *UserData {\n\traw := addressStore.Get(addr.String())\n\tif raw == nil {\n\t\treturn nil\n\t}\n\n\tdata := raw.(*UserData)\n\tif data.deleted {\n\t\treturn nil\n\t}\n\n\treturn data\n}\n\n// ResolveAny tries to resolve any given string to *UserData\n// If the input is not found in the registry in any form, nil is returned\nfunc ResolveAny(input string) (*UserData, bool) {\n\taddr := address(input)\n\tif addr.IsValid() {\n\t\treturn ResolveAddress(addr), true\n\t}\n\n\treturn ResolveName(input)\n}\n\n// GetReadonlyAddrStore exposes the address store in readonly mode\nfunc GetReadonlyAddrStore() *rotree.ReadOnlyTree {\n\treturn rotree.Wrap(addressStore, makeUserDataSafe)\n}\n\n// GetReadOnlyNameStore exposes the name store in readonly mode\nfunc GetReadOnlyNameStore() *rotree.ReadOnlyTree {\n\treturn rotree.Wrap(nameStore, makeUserDataSafe)\n}\n\nfunc makeUserDataSafe(data any) any {\n\tcpy := new(UserData)\n\t*cpy = *(data.(*UserData))\n\tif cpy.deleted {\n\t\treturn nil\n\t}\n\n\t// Note: when requesting data from this AVL tree, (exists bool) will be true\n\t// Even if the data is \"deleted\". This is currently unavoidable\n\treturn cpy\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"social","path":"gno.land/r/berty/social","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/berty/social\"\ngno = \"0.9\"\n\n"},{"name":"post.gno","body":"package social\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n//----------------------------------------\n// Post\n\n// NOTE: a PostID is relative to the userPosts.\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\n// Reaction is for the \"enum\" of ways to react to a post\ntype Reaction int\n\nconst (\n\tGnod Reaction = iota\n\tMaxReaction\n)\n\n// A Post is a \"thread\" or a \"reply\" depending on context.\n// A thread is a Post of a UserPosts that holds other replies.\n// This is similar to boards.Post except that this doesn't have a title.\ntype Post struct {\n\tuserPosts  *UserPosts\n\tid         PostID\n\tcreator    address\n\tbody       string\n\treplies    bptree.BPTree  // PostID -\u003e *Post\n\trepliesAll bptree.BPTree  // PostID -\u003e *Post (all replies, for top-level posts)\n\treposts    bptree.BPTree  // UserPosts user address -\u003e PostID\n\tthreadID   PostID         // original PostID\n\tparentID   PostID         // parent PostID (if reply or repost)\n\trepostUser address        // UserPosts user address of original post (if repost)\n\treactions  *bptree.BPTree // Reaction -\u003e *bptree.BPTree of address -\u003e \"\" (Use the bptree.BPTree keys as the \"set\" of addresses)\n\tcreatedAt  time.Time\n}\n\nfunc newPost(userPosts *UserPosts, id PostID, creator address, body string, threadID, parentID PostID, repostUser address) *Post {\n\treturn \u0026Post{\n\t\tuserPosts:  userPosts,\n\t\tid:         id,\n\t\tcreator:    creator,\n\t\tbody:       body,\n\t\treplies:    bptree.BPTree{},\n\t\trepliesAll: bptree.BPTree{},\n\t\treposts:    bptree.BPTree{},\n\t\tthreadID:   threadID,\n\t\tparentID:   parentID,\n\t\trepostUser: repostUser,\n\t\treactions:  bptree.NewBPTree32(),\n\t\tcreatedAt:  time.Now(),\n\t}\n}\n\nfunc (post *Post) IsThread() bool {\n\treturn post.parentID == 0\n}\n\nfunc (post *Post) GetPostID() PostID {\n\treturn post.id\n}\n\nfunc (post *Post) AddReply(creator address, body string) *Post {\n\tuserPosts := post.userPosts\n\tpid := userPosts.incGetPostID()\n\tpidkey := postIDKey(pid)\n\treply := newPost(userPosts, pid, creator, body, post.threadID, post.id, \"\")\n\tpost.replies.Set(pidkey, reply)\n\tif post.threadID == post.id {\n\t\tpost.repliesAll.Set(pidkey, reply)\n\t} else {\n\t\tthread := userPosts.GetThread(post.threadID)\n\t\tthread.repliesAll.Set(pidkey, reply)\n\t}\n\treturn reply\n}\n\nfunc (post *Post) AddRepostTo(creator address, comment string, dst *UserPosts) *Post {\n\tif !post.IsThread() {\n\t\tpanic(\"cannot repost non-thread post\")\n\t}\n\n\tpid := dst.incGetPostID()\n\tpidkey := postIDKey(pid)\n\trepost := newPost(dst, pid, creator, comment, pid, post.id, post.userPosts.userAddr)\n\tdst.threads.Set(pidkey, repost)\n\t// Also add to the home posts.\n\tdst.homePosts.Set(pidkey, repost)\n\tpost.reposts.Set(creator.String(), pid)\n\treturn repost\n}\n\nfunc (post *Post) GetReply(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\treplyI, ok := post.repliesAll.Get(pidkey)\n\tif !ok {\n\t\treturn nil\n\t} else {\n\t\treturn replyI.(*Post)\n\t}\n}\n\n// Add the userAddr to the posts.reactions for reaction.\n// Create the reaction key in post.reactions if needed.\n// If userAddr is already added, do nothing.\n// If the userAddr is the post's creator, do nothing. (Don't react to one's own posts.)\n// Return a boolean indicating whether the userAddr was added (false if it was already added).\nfunc (post *Post) AddReaction(userAddr address, reaction Reaction) bool {\n\tvalidateReaction(reaction)\n\n\tif userAddr == post.creator {\n\t\t// Don't react to one's own posts.\n\t\treturn false\n\t}\n\tvalue := getOrCreateReactionValue(post.reactions, reaction)\n\tif value.Has(userAddr.String()) {\n\t\t// Already added.\n\t\treturn false\n\t}\n\n\tvalue.Set(userAddr.String(), \"\")\n\treturn true\n}\n\n// Remove the userAddr from the posts.reactions for reaction.\n// If userAddr is already removed, do nothing.\n// Return a boolean indicating whether the userAddr was found and removed.\nfunc (post *Post) RemoveReaction(userAddr address, reaction Reaction) bool {\n\tvalidateReaction(reaction)\n\n\tif !post.reactions.Has(reactionKey(reaction)) {\n\t\t// There is no entry for reaction, so don't create one.\n\t\treturn false\n\t}\n\n\t_, removed := getOrCreateReactionValue(post.reactions, reaction).Remove(userAddr.String())\n\treturn removed\n}\n\n// Return the count of reactions for the reaction.\nfunc (post *Post) GetReactionCount(reaction Reaction) int {\n\tkey := reactionKey(reaction)\n\tvalueI, exists := post.reactions.Get(key)\n\tif exists {\n\t\treturn valueI.(*bptree.BPTree).Size()\n\t} else {\n\t\treturn 0\n\t}\n}\n\nfunc validateReaction(reaction Reaction) {\n\tif reaction \u003c 0 || reaction \u003e= MaxReaction {\n\t\tpanic(\"invalid Reaction value: \" + strconv.Itoa(int(reaction)))\n\t}\n}\n\nfunc (post *Post) GetSummary() string {\n\treturn summaryOf(post.body, 80)\n}\n\nfunc (post *Post) GetURL() string {\n\tif post.IsThread() {\n\t\treturn post.userPosts.GetURLFromThreadAndReplyID(\n\t\t\tpost.id, 0)\n\t} else {\n\t\treturn post.userPosts.GetURLFromThreadAndReplyID(\n\t\t\tpost.threadID, post.id)\n\t}\n}\n\nfunc (post *Post) GetGnodFormURL() string {\n\treturn txlink.Call(\"AddReaction\",\n\t\t\"userPostsAddr\", post.userPosts.userAddr.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t\t\"reaction\", strconv.Itoa(int(Gnod)))\n}\n\nfunc (post *Post) GetReplyFormURL() string {\n\treturn txlink.Call(\"PostReply\",\n\t\t\"userPostsAddr\", post.userPosts.userAddr.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String())\n}\n\nfunc (post *Post) GetRepostFormURL() string {\n\treturn txlink.Call(\"RepostThread\",\n\t\t\"userPostsAddr\", post.userPosts.userAddr.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String())\n}\n\nfunc (post *Post) RenderSummary() string {\n\tif post.repostUser != \"\" {\n\t\tdstUserPosts := getUserPosts(post.repostUser)\n\t\tif dstUserPosts == nil {\n\t\t\tpanic(\"repost user does not exist\")\n\t\t}\n\t\tthread := dstUserPosts.GetThread(PostID(post.parentID))\n\t\tif thread == nil {\n\t\t\treturn \"reposted post does not exist\"\n\t\t}\n\t\treturn \"Repost: \" + post.GetSummary() + \"\\n\\n\" + thread.RenderSummary()\n\t}\n\tstr := \"\"\n\tstr += post.GetSummary() + \"\\n\"\n\tstr += \"\\\\- \" + displayAddressMD(post.creator) + \",\"\n\tstr += \" [\" + post.createdAt.Format(\"2006-01-02 3:04pm MST\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" (\" + strconv.Itoa(post.GetReactionCount(Gnod)) + \" gnods)\"\n\tstr += \" (\" + strconv.Itoa(post.replies.Size()) + \" replies)\"\n\tstr += \" (\" + strconv.Itoa(post.reposts.Size()) + \" reposts)\" + \"\\n\"\n\treturn str\n}\n\nfunc (post *Post) RenderPost(indent string, levels int) string {\n\tif post == nil {\n\t\treturn \"nil post\"\n\t}\n\tstr := \"\"\n\tstr += indentBody(indent, post.body) + \"\\n\" // TODO: indent body lines.\n\tstr += indent + \"\\\\- \" + displayAddressMD(post.creator) + \", \"\n\tstr += \"[\" + post.createdAt.Format(\"2006-01-02 3:04pm (MST)\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" - (\" + strconv.Itoa(post.GetReactionCount(Gnod)) + \" gnods) - \"\n\tstr += \" \\\\[[gnod](\" + post.GetGnodFormURL() + \")]\"\n\tstr += \" \\\\[[reply](\" + post.GetReplyFormURL() + \")]\"\n\tif post.IsThread() {\n\t\tstr += \" \\\\[[repost](\" + post.GetRepostFormURL() + \")]\"\n\t}\n\tstr += \"\\n\"\n\tif levels \u003e 0 {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tpost.replies.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\t\tstr += indent + \"\\n\"\n\t\t\t\tstr += value.(*Post).RenderPost(indent+\"\u003e \", levels-1)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t} else {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tstr += indent + \"\\n\"\n\t\t\tstr += indent + \"_[see all \" + strconv.Itoa(post.replies.Size()) + \" replies](\" + post.GetURL() + \")_\\n\"\n\t\t}\n\t}\n\treturn str\n}\n\n// render reply and link to context thread\nfunc (post *Post) RenderInner() string {\n\tif post.IsThread() {\n\t\tpanic(\"unexpected thread\")\n\t}\n\tthreadID := post.threadID\n\t// replyID := post.id\n\tparentID := post.parentID\n\tstr := \"\"\n\tstr += \"_[see thread](\" + post.userPosts.GetURLFromThreadAndReplyID(\n\t\tthreadID, 0) + \")_\\n\\n\"\n\tthread := post.userPosts.GetThread(post.threadID)\n\tvar parent *Post\n\tif thread.id == parentID {\n\t\tparent = thread\n\t} else {\n\t\tparent = thread.GetReply(parentID)\n\t}\n\tstr += parent.RenderPost(\"\", 0)\n\tstr += \"\\n\"\n\tstr += post.RenderPost(\"\u003e \", 5)\n\treturn str\n}\n\n// MarshalJSON implements the json.Marshaler interface.\nfunc (post *Post) MarshalJSON() ([]byte, error) {\n\tcreatedAt, err := post.createdAt.MarshalJSON()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjson := new(bytes.Buffer)\n\n\tjson.WriteString(ufmt.Sprintf(`{\"id\": %d, \"createdAt\": %s, \"creator\": \"%s\", \"n_gnods\": %d, \"n_replies\": %d, \"n_replies_all\": %d, \"parent_id\": %d`,\n\t\tuint64(post.id), string(createdAt), post.creator.String(), post.GetReactionCount(Gnod), post.replies.Size(), post.repliesAll.Size(),\n\t\tuint64(post.parentID)))\n\tif post.repostUser != \"\" {\n\t\tjson.WriteString(ufmt.Sprintf(`, \"repost_user\": %s`, strconv.Quote(post.repostUser.String())))\n\t}\n\tjson.WriteString(ufmt.Sprintf(`, \"body\": %s}`, strconv.Quote(post.body)))\n\n\treturn json.Bytes(), nil\n}\n\nfunc getPosts(posts bptree.BPTree, startIndex int, endIndex int) string {\n\tjson := ufmt.Sprintf(\"{\\\"n_threads\\\": %d, \\\"posts\\\": [\\n  \", posts.Size())\n\n\tfor i := startIndex; i \u003c endIndex \u0026\u0026 i \u003c posts.Size(); i++ {\n\t\tif i \u003e startIndex {\n\t\t\tjson += \",\\n  \"\n\t\t}\n\n\t\t_, postI := posts.GetByIndex(i)\n\t\tpost := postI.(*Post)\n\t\tpostJson, err := post.MarshalJSON()\n\t\tif err != nil {\n\t\t\tpanic(\"can't get post JSON\")\n\t\t}\n\t\tjson += ufmt.Sprintf(\"{\\\"index\\\": %d, \\\"post\\\": %s}\", i, string(postJson))\n\t}\n\n\tjson += \"]}\"\n\treturn json\n}\n"},{"name":"public.gno","body":"package social\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\ntype UserAndPostID struct {\n\tUserPostAddr address\n\tPostID       PostID\n}\n\n// Post a message to the caller's main user posts.\n// The caller must already be registered with /r/gnoland/users/v1 Register.\n// Return the \"thread ID\" of the new post.\n// (This is similar to boards.CreateThread, but no message title)\nfunc PostMessage(cur realm, body string) PostID {\n\tcaller := cur.Previous().Address()\n\tuserPosts := getOrCreateUserPosts(caller, usernameOf(caller))\n\tthread := userPosts.AddThread(body)\n\treturn thread.id\n}\n\n// Post a reply to the user posts of userPostsAddr where threadid is the ID returned by\n// the original call to PostMessage. If postid == threadid then create another top-level\n// post for the threadid, otherwise post a reply to the postid \"sub reply\".\n// The caller must already be registered with /r/gnoland/users/v1 Register.\n// Return the new post ID.\n// (This is similar to boards.CreateReply.)\nfunc PostReply(cur realm, userPostsAddr address, threadid, postid PostID, body string) PostID {\n\tcaller := cur.Previous().Address()\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\tthread := userPosts.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"threadid in user posts does not exist\")\n\t}\n\tif postid == threadid {\n\t\treply := thread.AddReply(caller, body)\n\t\treturn reply.id\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"postid does not exist\")\n\t\t}\n\t\treply := post.AddReply(caller, body)\n\t\treturn reply.id\n\t}\n}\n\n// Repost the message from the user posts of userPostsAddr where threadid is the ID returned by\n// the original call to PostMessage. This must be a top-level thread (not a reply).\n// Return the new post ID.\n// (This is similar to boards.CreateRepost.)\nfunc RepostThread(cur realm, userPostsAddr address, threadid PostID, comment string) PostID {\n\tcaller := cur.Previous().Address()\n\tif userPostsAddr == caller {\n\t\tpanic(\"Cannot repost a user's own message\")\n\t}\n\n\tdstUserPosts := getOrCreateUserPosts(caller, usernameOf(caller))\n\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\tthread := userPosts.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"threadid in user posts does not exist\")\n\t}\n\trepost := thread.AddRepostTo(caller, comment, dstUserPosts)\n\treturn repost.id\n}\n\n// For each address/PostID in addrAndIDs, get the thread post. The Post ID must be\n// for a a top-level thread (not a reply; to get reply posts, use GetThreadPosts).\n// If the Post ID is not found, set the result for that Post ID to {}.\n// The response is a JSON string.\nfunc GetJsonTopPostsByID(addrAndIDs []UserAndPostID) string {\n\tjson := \"[ \"\n\tfor _, addrAndID := range addrAndIDs {\n\t\tif len(json) \u003e 2 {\n\t\t\tjson += \",\\n  \"\n\t\t}\n\n\t\tuserPosts := getUserPosts(addrAndID.UserPostAddr)\n\t\tif userPosts == nil {\n\t\t\tjson += \"{}\"\n\t\t\tcontinue\n\t\t}\n\n\t\tpost := userPosts.GetThread(PostID(addrAndID.PostID))\n\t\tif post == nil {\n\t\t\tjson += \"{}\"\n\t\t\tcontinue\n\t\t}\n\n\t\tpostJson, err := post.MarshalJSON()\n\t\tif err != nil {\n\t\t\tpanic(\"can't get post JSON\")\n\t\t}\n\t\tjson += string(postJson)\n\t}\n\tjson += \"]\"\n\n\treturn json\n}\n\n// Get posts in a thread for a user. A thread is the sequence of posts without replies.\n// While each post has an an arbitrary id, it also has an index within the thread starting from 0.\n// Limit the response to posts from startIndex up to (not including) endIndex within the thread.\n// If you just want the total count, set startIndex and endIndex to 0 and see the response \"n_threads\".\n// If threadID is 0 then return the user's top-level posts. (Like render args \"user\".)\n// If threadID is X and replyID is 0, then return the posts (without replies) in that thread. (Like render args \"user/2\".)\n// If threadID is X and replyID is Y, then return the posts in the thread starting with replyID. (Like render args \"user/2/5\".)\n// The response includes reposts by this user (only if threadID is 0), but not messages of other\n// users that are being followed. (See GetHomePosts.) The response is a JSON string.\nfunc GetThreadPosts(userPostsAddr address, threadID int, replyID int, startIndex int, endIndex int) string {\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\n\tif threadID == 0 {\n\t\treturn getPosts(userPosts.threads, startIndex, endIndex)\n\t}\n\n\tthread := userPosts.GetThread(PostID(threadID))\n\tif thread == nil {\n\t\tpanic(ufmt.Sprintf(\"thread does not exist with id %d\", threadID))\n\t}\n\n\tif replyID == 0 {\n\t\treturn getPosts(thread.replies, startIndex, endIndex)\n\t} else {\n\t\treply := thread.GetReply(PostID(replyID))\n\t\tif reply == nil {\n\t\t\tpanic(ufmt.Sprintf(\"reply does not exist with id %d in thread with id %d\", replyID, threadID))\n\t\t}\n\n\t\treturn getPosts(reply.replies, startIndex, endIndex)\n\t}\n}\n\n// Update the home posts by scanning all posts from all followed users and adding the\n// followed posts since the last call to RefreshHomePosts (or since started following the user).\n// Return the new count of home posts. The result is something like \"(12 int)\".\nfunc RefreshHomePosts(_ realm, userPostsAddr address) int {\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\tuserPosts.refreshHomePosts()\n\n\treturn userPosts.homePosts.Size()\n}\n\n// Get the number of posts which GetHomePosts or GetJsonHomePosts will return.\n// The result is something like \"(12 int)\".\n// This returns the current count of the home posts (without need to pay gas). To include the\n// latest followed posts, call RefreshHomePosts.\nfunc GetHomePostsCount(userPostsAddr address) int {\n\treturn GetHomePosts(userPostsAddr).Size()\n}\n\n// Get home posts for a user, which are the user's top-level posts plus all posts of all\n// users being followed.\n// The response is a map of postID -\u003e *Post. The bptree.BPTree sorts by the post ID which is\n// unique for every post and increases in time.\n// If you just want the total count, use GetHomePostsCount.\n// This returns the current state of the home posts (without need to pay gas). To include the\n// latest followed posts, call RefreshHomePosts.\nfunc GetHomePosts(userPostsAddr address) *bptree.BPTree {\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\treturn \u0026userPosts.homePosts\n}\n\n// Get home posts for a user (using GetHomePosts), which are the user's top-level posts plus all\n// posts of all users being followed.\n// Limit the response to posts from startIndex up to (not including) endIndex within the home posts.\n// If you just want the total count, use GetHomePostsCount.\n// The response is a JSON string.\n// This returns the current state of the home posts (without need to pay gas). To include the\n// latest posts, call RefreshHomePosts.\nfunc GetJsonHomePosts(userPostsAddr address, startIndex int, endIndex int) string {\n\tallPosts := GetHomePosts(userPostsAddr)\n\tpostsJson := \"\"\n\tfor i := startIndex; i \u003c endIndex \u0026\u0026 i \u003c allPosts.Size(); i++ {\n\t\t_, postI := allPosts.GetByIndex(i)\n\t\tif postsJson != \"\" {\n\t\t\tpostsJson += \",\\n  \"\n\t\t}\n\n\t\tpostJson, err := postI.(*Post).MarshalJSON()\n\t\tif err != nil {\n\t\t\tpanic(\"can't get post JSON\")\n\t\t}\n\t\tpostsJson += ufmt.Sprintf(\"{\\\"index\\\": %d, \\\"post\\\": %s}\", int(i), string(postJson))\n\t}\n\n\treturn ufmt.Sprintf(\"{\\\"n_posts\\\": %d, \\\"posts\\\": [\\n  %s]}\", allPosts.Size(), postsJson)\n}\n\n// Update the caller to follow the user with followedAddr. See UserPosts.Follow.\nfunc Follow(cur realm, followedAddr address) PostID {\n\tcaller := cur.Previous().Address()\n\tif followedAddr == caller {\n\t\tpanic(\"you can't follow yourself\")\n\t}\n\n\t// A user can follow someone before doing any posts, so create the UserPosts if needed.\n\tuserPosts := getOrCreateUserPosts(caller, usernameOf(caller))\n\treturn userPosts.Follow(followedAddr)\n}\n\n// Update the caller to unfollow the user with followedAddr. See UserPosts.Unfollow.\nfunc Unfollow(cur realm, followedAddr address) {\n\tcaller := cur.Previous().Address()\n\tuserPosts := getUserPosts(caller)\n\tif userPosts == nil {\n\t\t// We don't expect this, but just do nothing.\n\t\treturn\n\t}\n\n\tuserPosts.Unfollow(followedAddr)\n}\n\n// Add the reaction by the caller to the post of userPostsAddr, where threadid is the ID\n// returned by the original call to PostMessage. If postid == threadid then add the reaction\n// to a top-level post for the threadid, otherwise add the reaction to the postid \"sub reply\".\n// (This function's arguments are similar to PostReply.)\n// The caller must already be registered with /r/gnoland/users/v1 Register.\n// Return a boolean indicating whether the userAddr was added. See Post.AddReaction.\nfunc AddReaction(cur realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {\n\tcaller := cur.Previous().Address()\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\tthread := userPosts.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"threadid in user posts does not exist\")\n\t}\n\tif postid == threadid {\n\t\treturn thread.AddReaction(caller, reaction)\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"postid does not exist\")\n\t\t}\n\t\treturn post.AddReaction(caller, reaction)\n\t}\n}\n\n// Remove the reaction by the caller to the post of userPostsAddr, where threadid is the ID\n// returned by the original call to PostMessage. If postid == threadid then remove the reaction\n// from a top-level post for the threadid, otherwise remove the reaction from the postid \"sub reply\".\n// (This function's arguments are similar to PostReply.)\n// The caller must already be registered with /r/gnoland/users/v1 Register.\n// Return a boolean indicating whether the userAddr was removed. See Post.RemoveReaction.\nfunc RemoveReaction(cur realm, userPostsAddr address, threadid, postid PostID, reaction Reaction) bool {\n\tcaller := cur.Previous().Address()\n\tuserPosts := getUserPosts(userPostsAddr)\n\tif userPosts == nil {\n\t\tpanic(\"posts for userPostsAddr do not exist\")\n\t}\n\tthread := userPosts.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"threadid in user posts does not exist\")\n\t}\n\tif postid == threadid {\n\t\treturn thread.RemoveReaction(caller, reaction)\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"postid does not exist\")\n\t\t}\n\t\treturn post.RemoveReaction(caller, reaction)\n\t}\n}\n\n// Call users.ResolveAddress and return the result as JSON, or \"\" if not found.\n// (This is a temporary utility until gno.land supports returning structured data directly.)\nfunc GetJsonUserByAddress(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t}\n\n\treturn marshalJsonUser(user)\n}\n\n// Call users.ResolveName and return the result as JSON, or \"\" if not found.\n// (This is a temporary utility until gno.land supports returning structured data directly.)\nfunc GetJsonUserByName(name string) string {\n\tuser, _ := users.ResolveName(name)\n\tif user == nil {\n\t\treturn \"\"\n\t}\n\n\treturn marshalJsonUser(user)\n}\n\n// Get the UserPosts info for the user with the given address, including\n// url, n_threads, n_followers and n_following. If the user address is not\n// found, return \"\". The name of this function has \"Info\" because it just returns\n// the number of items, not the items themselves. To get the items, see\n// GetJsonFollowers, etc.\n// The response is a JSON string.\nfunc GetJsonUserPostsInfo(address address) string {\n\tuserPosts := getUserPosts(address)\n\tif userPosts == nil {\n\t\treturn \"\"\n\t}\n\n\tjson, err := userPosts.MarshalJSON()\n\tif err != nil {\n\t\tpanic(\"can't get UserPosts JSON\")\n\t}\n\n\treturn string(json)\n}\n\n// Get the UserPosts for the user with the given address, and return\n// the list of followers. If the user address is not found, return \"\".\n// Limit the response to entries from startIndex up to (not including) endIndex.\n// The response is a JSON string.\nfunc GetJsonFollowers(address address, startIndex int, endIndex int) string {\n\tuserPosts := getUserPosts(address)\n\tif userPosts == nil {\n\t\treturn \"\"\n\t}\n\n\tjson := ufmt.Sprintf(\"{\\\"n_followers\\\": %d, \\\"followers\\\": [\\n  \", userPosts.followers.Size())\n\tfor i := startIndex; i \u003c endIndex \u0026\u0026 i \u003c userPosts.followers.Size(); i++ {\n\t\taddr, _ := userPosts.followers.GetByIndex(i)\n\n\t\tif i \u003e startIndex {\n\t\t\tjson += \",\\n  \"\n\t\t}\n\t\tjson += ufmt.Sprintf(`{\"address\": \"%s\"}`, addr)\n\t}\n\tjson += \"]}\"\n\n\treturn json\n}\n\n// Get the UserPosts for the user with the given address, and return\n// the list of other users that this user is following.\n// If the user address is not found, return \"\".\n// Limit the response to entries from startIndex up to (not including) endIndex.\n// The response is a JSON string.\nfunc GetJsonFollowing(address address, startIndex int, endIndex int) string {\n\tuserPosts := getUserPosts(address)\n\tif userPosts == nil {\n\t\treturn \"\"\n\t}\n\n\tjson := ufmt.Sprintf(\"{\\\"n_following\\\": %d, \\\"following\\\": [\\n  \", userPosts.following.Size())\n\tfor i := startIndex; i \u003c endIndex \u0026\u0026 i \u003c userPosts.following.Size(); i++ {\n\t\taddr, infoI := userPosts.following.GetByIndex(i)\n\n\t\tif i \u003e startIndex {\n\t\t\tjson += \",\\n  \"\n\t\t}\n\t\tstartedAt, err := infoI.(*FollowingInfo).startedFollowingAt.MarshalJSON()\n\t\tif err != nil {\n\t\t\tpanic(\"can't get startedFollowingAt JSON\")\n\t\t}\n\t\tjson += ufmt.Sprintf(`{\"address\": \"%s\", \"started_following_at\": %s}`,\n\t\t\taddr, string(startedAt))\n\t}\n\tjson += \"]}\"\n\n\treturn json\n}\n\n// Get a list of user names starting from the given prefix. Limit the\n// number of results to maxResults.\nfunc ListUsersByPrefix(prefix string, maxResults int) []string {\n\treturn listByteStringKeysByPrefix(\u0026gUserAddressByName, prefix, maxResults)\n}\n\n// Get a list of user names starting from the given prefix. Limit the\n// number of results to maxResults.\n// The response is a JSON string.\nfunc ListJsonUsersByPrefix(prefix string, maxResults int) string {\n\tnames := ListUsersByPrefix(prefix, maxResults)\n\n\tjson := \"[\"\n\tfor i, name := range names {\n\t\tif i \u003e 0 {\n\t\t\tjson += \", \"\n\t\t}\n\t\tjson += strconv.Quote(name)\n\t}\n\tjson += \"]\"\n\treturn json\n}\n"},{"name":"render.gno","body":"package social\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// resolveAddr looks up an address by registered name first, then falls back\n// to treating the input as a raw address. This allows unregistered users to\n// be found by their address string.\nfunc resolveAddr(nameOrAddr string) address {\n\tuser, _ := users.ResolveName(nameOrAddr)\n\tif user != nil {\n\t\treturn user.Addr()\n\t}\n\treturn address(nameOrAddr)\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"Welcome to dSocial!\\n\\n\"\n\n\t\t// List the users who have posted. gUserAddressByName is already sorted by name.\n\t\tgUserAddressByName.Iterate(\"\", \"\", func(name string, value interface{}) bool {\n\t\t\tstr += \" * [@\" + name + \"](\" + gRealmPath + \":\" + name + \")\" + \"\\n\"\n\t\t\treturn false\n\t\t})\n\n\t\treturn str\n\t}\n\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/berty/social:USER_NAME_OR_ADDR\n\t\tuserAddr := resolveAddr(path)\n\t\tuserPosts := getUserPosts(userAddr)\n\t\tif userPosts == nil {\n\t\t\treturn \"No posts by: \" + path\n\t\t}\n\n\t\treturn userPosts.RenderUserPosts(false)\n\t} else if len(parts) == 2 {\n\t\tuserAddr := resolveAddr(parts[0])\n\t\tuserPosts := getUserPosts(userAddr)\n\t\tif userPosts == nil {\n\t\t\treturn \"No posts by: \" + parts[0]\n\t\t}\n\n\t\tif parts[1] == \"home\" {\n\t\t\t// /r/berty/social:USER_NAME_OR_ADDR/home\n\t\t\treturn userPosts.RenderUserPosts(true)\n\t\t} else if parts[1] == \"followers\" {\n\t\t\t// /r/berty/social:USER_NAME_OR_ADDR/followers\n\t\t\treturn userPosts.RenderFollowers()\n\t\t} else if parts[1] == \"following\" {\n\t\t\t// /r/berty/social:USER_NAME_OR_ADDR/following\n\t\t\treturn userPosts.RenderFollowing()\n\t\t} else {\n\t\t\t// /r/berty/social:USER_NAME_OR_ADDR/THREAD_ID\n\t\t\tpid, err := strconv.Atoi(parts[1])\n\t\t\tif err != nil {\n\t\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t\t}\n\t\t\tthread := userPosts.GetThread(PostID(pid))\n\t\t\tif thread == nil {\n\t\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t\t}\n\t\t\treturn thread.RenderPost(\"\", 5)\n\t\t}\n\t} else if len(parts) == 3 {\n\t\t// /r/berty/social:USER_NAME_OR_ADDR/THREAD_ID/REPLY_ID\n\t\tuserAddr := resolveAddr(parts[0])\n\t\tuserPosts := getUserPosts(userAddr)\n\t\tif userPosts == nil {\n\t\t\treturn \"No posts by: \" + parts[0]\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tthread := userPosts.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\trid, err := strconv.Atoi(parts[2])\n\t\tif err != nil {\n\t\t\treturn \"invalid reply id: \" + parts[2]\n\t\t}\n\t\treply := thread.GetReply(PostID(rid))\n\t\tif reply == nil {\n\t\t\treturn \"reply does not exist with id: \" + parts[2]\n\t\t}\n\t\treturn reply.RenderInner()\n\t} else {\n\t\treturn \"unrecognized path: \" + path\n\t}\n}\n"},{"name":"social.gno","body":"package social\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nvar (\n\tgUserPostsByAddress bptree.BPTree // user's address -\u003e *UserPosts\n\tgUserAddressByName  bptree.BPTree // user's username -\u003e address\n\tpostsCtr            uint64   // increments Post.id globally\n\n\t// gRealmPath is the realm's relative URL path (e.g. \"/r/g1.../social\"),\n\t// derived from the deployed package path at init time.\n\tgRealmPath string\n)\n\nfunc init(cur realm) {\n\tpkgPath := cur.PkgPath()\n\t// Strip the chain domain (everything before the first \"/\") to get the\n\t// relative path suitable for markdown links: \"/r/g1.../social\".\n\tif idx := strings.Index(pkgPath, \"/\"); idx \u003e= 0 {\n\t\tgRealmPath = pkgPath[idx:]\n\t}\n}\n"},{"name":"userposts.gno","body":"package social\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\ntype FollowingInfo struct {\n\tstartedFollowingAt time.Time\n\tstartedPostsCtr    PostID\n}\n\n// UserPosts is similar to boards.Board where each user has their own \"board\" for\n// posts which come from the user. The list of posts is identified by the user's address .\n// A user's \"home feed\" may contain other posts (from followed users, etc.) but this only\n// has the top-level posts from the user (not replies to other user's posts).\ntype UserPosts struct {\n\turl           string\n\tuserAddr      address\n\tthreads       bptree.BPTree // PostID -\u003e *Post\n\thomePosts     bptree.BPTree // PostID -\u003e *Post. Includes this user's threads posts plus posts of users being followed.\n\tlastRefreshId PostID        // Updated by refreshHomePosts\n\tfollowers     bptree.BPTree // address -\u003e \"\"\n\tfollowing     bptree.BPTree // address -\u003e *FollowingInfo\n}\n\n// Create a new userPosts for the user. Panic if there is already a userPosts for the user.\nfunc newUserPosts(url string, userAddr address) *UserPosts {\n\tif gUserPostsByAddress.Has(userAddr.String()) {\n\t\tpanic(\"userPosts already exists\")\n\t}\n\treturn \u0026UserPosts{\n\t\turl:           url,\n\t\tuserAddr:      userAddr,\n\t\tthreads:       bptree.BPTree{},\n\t\thomePosts:     bptree.BPTree{},\n\t\tlastRefreshId: PostID(postsCtr), // Ignore past messages of followed users.\n\t\tfollowers:     bptree.BPTree{},\n\t\tfollowing:     bptree.BPTree{},\n\t}\n}\n\nfunc (userPosts *UserPosts) GetThread(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpostI, exists := userPosts.threads.Get(pidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn postI.(*Post)\n}\n\n// Add a new top-level thread to the userPosts. Return the new Post.\nfunc (userPosts *UserPosts) AddThread(body string) *Post {\n\tpid := userPosts.incGetPostID()\n\tpidkey := postIDKey(pid)\n\tthread := newPost(userPosts, pid, userPosts.userAddr, body, pid, 0, \"\")\n\tuserPosts.threads.Set(pidkey, thread)\n\t// Also add to the home posts.\n\tuserPosts.homePosts.Set(pidkey, thread)\n\treturn thread\n}\n\n// If already following followedAddr, then do nothing and return 0.\n// If there is a UserPosts for followedAddr, then add it to following,\n// and add this user to its followers.\n// If there is no UserPosts for followedAddr, then do nothing and return 0. (We don't expect\n// this because this is usually called by clicking on the display page of followedAddr.)\n// Return the value of startedPostsCtr in the added FollowingInfo.\nfunc (userPosts *UserPosts) Follow(followedAddr address) PostID {\n\tif userPosts.following.Has(followedAddr.String()) {\n\t\t// Already following.\n\t\treturn PostID(0)\n\t}\n\n\tfollowedUserPosts := getUserPosts(followedAddr)\n\tif followedUserPosts != nil {\n\t\tuserPosts.following.Set(followedAddr.String(), \u0026FollowingInfo{\n\t\t\tstartedFollowingAt: time.Now(),\n\t\t\tstartedPostsCtr:    PostID(postsCtr), // Ignore past messages.\n\t\t})\n\t\tfollowedUserPosts.followers.Set(userPosts.userAddr.String(), \"\")\n\t\treturn PostID(postsCtr)\n\t}\n\n\treturn PostID(0)\n}\n\n// Remove followedAddr from following.\n// If there is a UserPosts for followedAddr, then remove this user from its followers.\n// If there is no UserPosts for followedAddr, then do nothing. (We don't expect this usually.)\nfunc (userPosts *UserPosts) Unfollow(followedAddr address) {\n\tuserPosts.following.Remove(followedAddr.String())\n\n\tfollowedUserPosts := getUserPosts(followedAddr)\n\tif followedUserPosts != nil {\n\t\tfollowedUserPosts.followers.Remove(userPosts.userAddr.String())\n\t}\n}\n\n// Renders the userPosts for display suitable as plaintext in\n// console.  This is suitable for demonstration or tests,\n// but not for prod.\nfunc (userPosts *UserPosts) RenderUserPosts(includeFollowed bool) string {\n\tstr := \"\"\n\tfollowers := strconv.Itoa(userPosts.followers.Size()) + \" Followers\"\n\tfollowing := \"Following \" + strconv.Itoa(userPosts.following.Size())\n\tuser := users.ResolveAddress(userPosts.userAddr)\n\tnameOrAddr := userPosts.userAddr.String()\n\tif user != nil {\n\t\tnameOrAddr = user.Name()\n\t}\n\tfollowers = \"[\" + followers + \"](\" + gRealmPath + \":\" + nameOrAddr + \"/followers)\"\n\tfollowing = \"[\" + following + \"](\" + gRealmPath + \":\" + nameOrAddr + \"/following)\"\n\tstr += followers + \" \u0026nbsp;\" + following + \"\\n\\n\"\n\n\tstr += \"\\\\[[post](\" + userPosts.GetPostFormURL() + \")] \\\\[[follow](\" + userPosts.GetFollowFormURL() + \")]\"\n\tif includeFollowed {\n\t\tstr += \" \\\\[[refresh](\" + userPosts.GetRefreshFormURL() + \")]\"\n\t}\n\tstr += \"\\n\\n\"\n\n\tvar posts *bptree.BPTree\n\tif includeFollowed {\n\t\tposts = \u0026userPosts.homePosts\n\t} else {\n\t\tposts = \u0026userPosts.threads\n\t}\n\tposts.ReverseIterate(\"\", \"\", func(key string, postI interface{}) bool {\n\t\tstr += \"----------------------------------------\\n\"\n\t\tstr += postI.(*Post).RenderSummary() + \"\\n\"\n\t\treturn false\n\t})\n\n\treturn str\n}\n\nfunc (userPosts *UserPosts) RenderFollowers() string {\n\tstr := \"\"\n\tuser := users.ResolveAddress(userPosts.userAddr)\n\townerRef := userPosts.userAddr.String()\n\tif user != nil {\n\t\townerRef = user.Name()\n\t}\n\tstr += \"[@\" + ownerRef + \"](\" + gRealmPath + \":\" + ownerRef + \") Followers\\n\\n\"\n\n\t// List the followers, sorted by name/addr.\n\tnames := []string{}\n\tuserPosts.followers.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif u := users.ResolveAddress(address(key)); u != nil {\n\t\t\tnames = append(names, u.Name())\n\t\t} else {\n\t\t\tnames = append(names, key)\n\t\t}\n\t\treturn false\n\t})\n\tsort.Strings(names)\n\tfor _, name := range names {\n\t\tstr += \" * [@\" + name + \"](\" + gRealmPath + \":\" + name + \")\" + \"\\n\"\n\t}\n\n\treturn str\n}\n\nfunc (userPosts *UserPosts) RenderFollowing() string {\n\tstr := \"\"\n\tuser := users.ResolveAddress(userPosts.userAddr)\n\townerRef := userPosts.userAddr.String()\n\tif user != nil {\n\t\townerRef = user.Name()\n\t}\n\tstr += \"[@\" + ownerRef + \"](\" + gRealmPath + \":\" + ownerRef + \") Following\\n\\n\"\n\n\t// List the following, sorted by name/addr.\n\tnameAddrs := []string{}\n\tuserPosts.following.Iterate(\"\", \"\", func(addr string, infoI interface{}) bool {\n\t\tinfo := infoI.(*FollowingInfo)\n\t\tref := addr\n\t\tif u := users.ResolveAddress(address(addr)); u != nil {\n\t\t\tref = u.Name()\n\t\t}\n\t\tnameAddrs = append(nameAddrs, ref+\"/\"+addr+\"/\"+info.startedFollowingAt.Format(\"2006-01-02\"))\n\t\treturn false\n\t})\n\tsort.Strings(nameAddrs)\n\tfor _, nameAddr := range nameAddrs {\n\t\tparts := strings.Split(nameAddr, \"/\")\n\t\tname := parts[0]\n\t\taddr := parts[1]\n\t\tsince := parts[2]\n\t\tstr += \" * [@\" + name + \"](\" + gRealmPath + \":\" + name + \") since \" + since +\n\t\t\t\"  \\\\[[unfollow](\" + userPosts.GetUnfollowFormURL(address(addr)) + \")]\\n\"\n\t}\n\n\treturn str\n}\n\nfunc (userPosts *UserPosts) incGetPostID() PostID {\n\tpostsCtr++\n\treturn PostID(postsCtr)\n}\n\nfunc (userPosts *UserPosts) GetURLFromThreadAndReplyID(threadID, replyID PostID) string {\n\tif replyID == 0 {\n\t\treturn userPosts.url + \"/\" + threadID.String()\n\t} else {\n\t\treturn userPosts.url + \"/\" + threadID.String() + \"/\" + replyID.String()\n\t}\n}\n\nfunc (userPosts *UserPosts) GetPostFormURL() string {\n\treturn txlink.Call(\"PostMessage\")\n}\n\nfunc (userPosts *UserPosts) GetFollowFormURL() string {\n\treturn txlink.Call(\"Follow\", \"followedAddr\", userPosts.userAddr.String())\n}\n\nfunc (userPosts *UserPosts) GetUnfollowFormURL(followedAddr address) string {\n\treturn txlink.Call(\"Unfollow\", \"followedAddr\", followedAddr.String())\n}\n\nfunc (userPosts *UserPosts) GetRefreshFormURL() string {\n\treturn txlink.Call(\"RefreshHomePosts\", \"userPostsAddr\", userPosts.userAddr.String())\n}\n\n// Scan userPosts.following for all posts from all followed users starting from lastRefreshId+1 .\n// Add the posts to the homePosts bptree.BPTree, which is sorted by the post ID which is unique for every post and\n// increases in time. When finished, update lastRefreshId.\nfunc (userPosts *UserPosts) refreshHomePosts() {\n\tminStartKey := postIDKey(userPosts.lastRefreshId + 1)\n\n\tuserPosts.following.Iterate(\"\", \"\", func(followedAddr string, infoI interface{}) bool {\n\t\tfollowedUserPosts := getUserPosts(address(followedAddr))\n\t\tif followedUserPosts == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tinfo := infoI.(*FollowingInfo)\n\t\tstartKey := minStartKey\n\t\tif info.startedPostsCtr \u003e userPosts.lastRefreshId {\n\t\t\t// Started following after the last refresh. Ignore messages before started following.\n\t\t\tstartKey = postIDKey(info.startedPostsCtr + 1)\n\t\t}\n\n\t\tfollowedUserPosts.threads.Iterate(startKey, \"\", func(id string, postI interface{}) bool {\n\t\t\tuserPosts.homePosts.Set(id, postI.(*Post))\n\t\t\treturn false\n\t\t})\n\n\t\treturn false\n\t})\n\n\tuserPosts.lastRefreshId = PostID(postsCtr)\n}\n\n// MarshalJSON implements the json.Marshaler interface.\nfunc (userPosts *UserPosts) MarshalJSON() ([]byte, error) {\n\tjson := new(bytes.Buffer)\n\tjson.WriteString(ufmt.Sprintf(`{\"address\": \"%s\", \"url\": %s, \"n_threads\": %d, \"n_followers\": %d, \"n_following\": %d}`,\n\t\tuserPosts.userAddr.String(), strconv.Quote(userPosts.url), userPosts.threads.Size(),\n\t\tuserPosts.followers.Size(), userPosts.following.Size()))\n\n\treturn json.Bytes(), nil\n}\n"},{"name":"util.gno","body":"package social\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/sys/users\"\n)\n\n//----------------------------------------\n// private utility methods\n\n// Get the UserPosts for the user.\nfunc getUserPosts(userAddr address) *UserPosts {\n\tuserPosts, exists := gUserPostsByAddress.Get(userAddr.String())\n\tif !exists {\n\t\treturn nil\n\t}\n\n\treturn userPosts.(*UserPosts)\n}\n\n// Get the UserPosts for the userAddr. If not found, add a new UserPosts to\n// gUserPostsByAddress and update gUserAddressByName with the username.\n// (The caller usually has already called usernameOf to get the username, but if\n// it is \"\" then this will get it.)\nfunc getOrCreateUserPosts(userAddr address, username string) *UserPosts {\n\tuserPosts := getUserPosts(userAddr)\n\tif userPosts != nil {\n\t\treturn userPosts\n\t}\n\n\tif username == \"\" {\n\t\tusername = usernameOf(userAddr)\n\t}\n\tif username == \"\" {\n\t\tusername = userAddr.String()\n\t}\n\n\tuserPosts = newUserPosts(gRealmPath+\":\"+username, userAddr)\n\tgUserPostsByAddress.Set(userAddr.String(), userPosts)\n\tgUserAddressByName.Set(username, userAddr)\n\n\treturn userPosts\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc reactionKey(reaction Reaction) string {\n\treturn strconv.Itoa(int(reaction))\n}\n\n// If reactions has an value for the given reaction, then return it.\n// Otherwise, add the reaction key to reactions, set the value to an empty bptree.BPTree and return it.\nfunc getOrCreateReactionValue(reactions *bptree.BPTree, reaction Reaction) *bptree.BPTree {\n\tkey := reactionKey(reaction)\n\tvalueI, exists := reactions.Get(key)\n\tif exists {\n\t\treturn valueI.(*bptree.BPTree)\n\t} else {\n\t\tvalue := bptree.NewBPTree32()\n\t\treactions.Set(key, value)\n\t\treturn value\n\t}\n}\n\n// listByteStringKeysByPrefix returns up to maxResults keys from tree that start\n// with the given prefix, treating keys as byte strings (not Unicode runes).\n// Inlined from gno.land/p/jefft0/avlhelpers which is not yet deployed.\nfunc listByteStringKeysByPrefix(tree bptree.ITree, prefix string, maxResults int) []string {\n\tresult := []string{}\n\tend := \"\"\n\tn := len(prefix)\n\tfor n \u003e 0 {\n\t\tif ascii := int(prefix[n-1]); ascii \u003c 0xff {\n\t\t\tend = prefix[0:n-1] + string(ascii+1)\n\t\t\tbreak\n\t\t}\n\t\tn--\n\t}\n\ttree.Iterate(prefix, end, func(key string, value any) bool {\n\t\tresult = append(result, key)\n\t\treturn len(result) \u003e= maxResults\n\t})\n\treturn result\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/r/gnoland/users/v1:\" + addr.String() + \")\"\n\t} else {\n\t\treturn \"[@\" + user.Name() + \"](\" + gRealmPath + \":\" + user.Name() + \")\"\n\t}\n}\n\nfunc usernameOf(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t} else {\n\t\treturn user.Name()\n\t}\n}\n\n// Return the User info as a JSON string.\n// (This is a temporary utility until gno.land supports returning structured data directly.)\nfunc marshalJsonUser(user *users.UserData) string {\n\treturn ufmt.Sprintf(\n\t\t\"{\\\"address\\\": \\\"%s\\\", \\\"name\\\": \\\"%s\\\", \\\"deleted\\\": %t}\",\n\t\tuser.Addr().String(), user.Name(), user.IsDeleted())\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ingester","path":"gno.land/p/demo/gnorkle/ingester","files":[{"name":"errors.gno","body":"package ingester\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"ingester undefined\")\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/ingester\"\ngno = \"0.9\"\n"},{"name":"type.gno","body":"package ingester\n\n// Type indicates an ingester type.\ntype Type int\n\nconst (\n\t// TypeSingle indicates an ingester that can only ingest a single within a given period or no period.\n\tTypeSingle Type = iota\n\t// TypeMulti indicates an ingester that can ingest multiple within a given period or no period\n\tTypeMulti\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"pager","path":"gno.land/p/nt/bptree/v0/pager","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/v0/pager\"\ngno = \"0.9\"\n"},{"name":"pager.gno","body":"package pager\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/bptree/v0/rotree\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Pager is a struct that holds the B+ tree and pagination parameters.\ntype Pager struct {\n\tTree            rotree.IReadOnlyTree\n\tPageQueryParam  string\n\tSizeQueryParam  string\n\tDefaultPageSize int\n\tReversed        bool\n}\n\n// Page represents a single page of results.\ntype Page struct {\n\tItems      []Item\n\tPageNumber int\n\tPageSize   int\n\tTotalItems int\n\tTotalPages int\n\tHasPrev    bool\n\tHasNext    bool\n\tPager      *Pager // Reference to the parent Pager\n}\n\n// Item represents a key-value pair in the B+ tree.\ntype Item struct {\n\tKey   string\n\tValue any\n}\n\n// NewPager creates a new Pager with default values.\nfunc NewPager(tree rotree.IReadOnlyTree, defaultPageSize int, reversed bool) *Pager {\n\treturn \u0026Pager{\n\t\tTree:            tree,\n\t\tPageQueryParam:  \"page\",\n\t\tSizeQueryParam:  \"size\",\n\t\tDefaultPageSize: defaultPageSize,\n\t\tReversed:        reversed,\n\t}\n}\n\n// GetPage retrieves a page of results from the B+ tree.\nfunc (p *Pager) GetPage(pageNumber int) *Page {\n\treturn p.GetPageWithSize(pageNumber, p.DefaultPageSize)\n}\n\nfunc (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {\n\ttotalItems := p.Tree.Size()\n\ttotalPages := int(math.Ceil(float64(totalItems) / float64(pageSize)))\n\n\tpage := \u0026Page{\n\t\tTotalItems: totalItems,\n\t\tTotalPages: totalPages,\n\t\tPageSize:   pageSize,\n\t\tPager:      p,\n\t}\n\n\t// pages without content\n\tif pageSize \u003c 1 {\n\t\treturn page\n\t}\n\n\t// page number provided is not available\n\tif pageNumber \u003c 1 {\n\t\tpage.HasNext = totalPages \u003e 0\n\t\treturn page\n\t}\n\n\t// page number provided is outside the range of total pages\n\tif pageNumber \u003e totalPages {\n\t\tpage.PageNumber = pageNumber\n\t\tpage.HasPrev = pageNumber \u003e 0\n\t\treturn page\n\t}\n\n\tstartIndex := (pageNumber - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\tif endIndex \u003e totalItems {\n\t\tendIndex = totalItems\n\t}\n\n\titems := []Item{}\n\n\tif p.Reversed {\n\t\tp.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value any) bool {\n\t\t\titems = append(items, Item{Key: key, Value: value})\n\t\t\treturn false\n\t\t})\n\t} else {\n\t\tp.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value any) bool {\n\t\t\titems = append(items, Item{Key: key, Value: value})\n\t\t\treturn false\n\t\t})\n\t}\n\n\tpage.Items = items\n\tpage.PageNumber = pageNumber\n\tpage.HasPrev = pageNumber \u003e 1\n\tpage.HasNext = pageNumber \u003c totalPages\n\treturn page\n}\n\nfunc (p *Pager) MustGetPageByPath(rawURL string) *Page {\n\tpage, err := p.GetPageByPath(rawURL)\n\tif err != nil {\n\t\tpanic(\"invalid path\")\n\t}\n\treturn page\n}\n\n// GetPageByPath retrieves a page of results based on the query parameters in the URL path.\nfunc (p *Pager) GetPageByPath(rawURL string) (*Page, error) {\n\tpageNumber, pageSize, err := p.ParseQuery(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.GetPageWithSize(pageNumber, pageSize), nil\n}\n\n// Picker generates the Markdown UI for the page Picker\nfunc (p *Page) Picker(path string) string {\n\tpageNumber := p.PageNumber\n\tpageNumber = max(pageNumber, 1)\n\n\tif p.TotalPages \u003c= 1 {\n\t\treturn \"\"\n\t}\n\n\tu, _ := url.Parse(path)\n\tquery := u.Query()\n\n\t// Remove existing page query parameter\n\tquery.Del(p.Pager.PageQueryParam)\n\n\t// Encode remaining query parameters\n\tbaseQuery := query.Encode()\n\tif baseQuery != \"\" {\n\t\tbaseQuery = \"\u0026\" + baseQuery\n\t}\n\tmd := \"\"\n\n\tif p.HasPrev {\n\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d%s) | \", 1, p.Pager.PageQueryParam, 1, baseQuery)\n\n\t\tif p.PageNumber \u003e 4 {\n\t\t\tmd += \"… | \"\n\t\t}\n\n\t\tif p.PageNumber \u003e 3 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d%s) | \", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2, baseQuery)\n\t\t}\n\n\t\tif p.PageNumber \u003e 2 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d%s) | \", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1, baseQuery)\n\t\t}\n\t}\n\n\tif p.PageNumber \u003e 0 \u0026\u0026 p.PageNumber \u003c= p.TotalPages {\n\t\tmd += ufmt.Sprintf(\"**%d**\", p.PageNumber)\n\t} else {\n\t\tmd += ufmt.Sprintf(\"_%d_\", p.PageNumber)\n\t}\n\n\tif p.HasNext {\n\t\tif p.PageNumber \u003c p.TotalPages-1 {\n\t\t\tmd += ufmt.Sprintf(\" | [%d](?%s=%d%s)\", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1, baseQuery)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-2 {\n\t\t\tmd += ufmt.Sprintf(\" | [%d](?%s=%d%s)\", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2, baseQuery)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-3 {\n\t\t\tmd += \" | …\"\n\t\t}\n\n\t\tmd += ufmt.Sprintf(\" | [%d](?%s=%d%s)\", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages, baseQuery)\n\t}\n\n\treturn md\n}\n\n// ParseQuery parses the URL to extract the page number and page size.\nfunc (p *Pager) ParseQuery(rawURL string) (int, int, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn 1, p.DefaultPageSize, err\n\t}\n\n\tquery := u.Query()\n\tpageNumber := 1\n\tpageSize := p.DefaultPageSize\n\n\tif p.PageQueryParam != \"\" {\n\t\tif pageStr := query.Get(p.PageQueryParam); pageStr != \"\" {\n\t\t\tpageNumber, err = strconv.Atoi(pageStr)\n\t\t\tif err != nil || pageNumber \u003c 1 {\n\t\t\t\tpageNumber = 1\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.SizeQueryParam != \"\" {\n\t\tif sizeStr := query.Get(p.SizeQueryParam); sizeStr != \"\" {\n\t\t\tpageSize, err = strconv.Atoi(sizeStr)\n\t\t\tif err != nil || pageSize \u003c 1 {\n\t\t\t\tpageSize = p.DefaultPageSize\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pageNumber, pageSize, nil\n}\n\nfunc max(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ownable","path":"gno.land/p/nt/ownable/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `ownable` - Ownership pattern for realms\n\nProvides an `Ownable` object that gates privileged operations behind a single owner address. Embed it in a realm (or any struct) to restrict actions like configuration changes, withdrawals, or upgrades.\n\n## Usage\n\n```go\npackage myrealm\n\nimport (\n    \"chain/runtime\"\n\n    \"gno.land/p/nt/ownable/v0\"\n)\n\n// The owner address is chosen explicitly at construction. A common\n// choice is the deployer, captured in init after confirming it is a\n// real user call.\nvar owner *ownable.Ownable\n\nfunc init() {\n    caller := runtime.PreviousRealm()\n    if !caller.IsUserCall() {\n        panic(\"must be deployed by a user\")\n    }\n    owner = ownable.NewWithAddress(caller.Address())\n}\n\n// SetFee is gated: only the current owner may call it.\nfunc SetFee(cur realm, newFee int64) {\n    if !cur.IsCurrent() {\n        panic(\"spoofed realm\")\n    }\n    owner.AssertOwnedBy(cur.Previous().Address())\n    fee = newFee\n}\n\n// Hand the realm over. TransferOwnership itself verifies the caller is owner.\nfunc TransferOwner(cur realm, to address) error {\n    return owner.TransferOwnership(0, cur, to)\n}\n```\n\nThere is no auth-mode flag. The single `NewWithAddress` constructor replaced the\nold `New` / `NewWithOrigin` / `NewWithAddressByPrevious` sugar: the realm now picks\nthe owner address explicitly rather than baking a runtime walk into the struct.\n\n## API\n\n```go\ntype Ownable struct{ /* unexported */ }\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\nvar (\n    ErrUnauthorized   = errors.New(\"ownable: caller is not owner\")\n    ErrInvalidAddress = errors.New(\"ownable: new owner address is invalid\")\n)\n\n// NewWithAddress is the only constructor: the realm picks the owner\n// address explicitly (e.g. cur.Previous().Address() after checking\n// cur.Previous().IsUserCall() in init).\nfunc NewWithAddress(addr address) *Ownable\n\n// Queries (caller supplies the address to check).\nfunc (o *Ownable) Owner() address             // \"\" if o is nil or ownership was dropped\nfunc (o *Ownable) OwnedBy(addr address) bool  // true if addr is the current owner\nfunc (o *Ownable) AssertOwnedBy(addr address) // panics with ErrUnauthorized if addr is not the owner\n\n// Authority mutation (thread the caller's own cur; pass 0 as the first arg).\nfunc (o *Ownable) TransferOwnership(_ int, rlm realm, newOwner address) error\nfunc (o *Ownable) DropOwnership(_ int, rlm realm) error // sets owner to \"\" — irreversible\n```\n\n## Notes\n\n- Authority-mutating methods assert `rlm.IsCurrent()` and identify the caller as `rlm.Previous().Address()`, which must equal the current owner. The principal is therefore unforgeable: an attacker cannot supply an arbitrary caller address. Pass `0` as the placeholder first arg and your own `cur` as `rlm`.\n- Read helpers (`OwnedBy`, `AssertOwnedBy`) take a bare address; the caller extracts it, guarding with `cur.IsCurrent()` before reading `cur.Previous().Address()`.\n- `TransferOwnership` rejects an invalid `newOwner` with `ErrInvalidAddress`. Both mutators emit `OwnershipTransferEvent` with `from` and `to` fields.\n- `DropOwnership` is permanent: `owner` becomes `\"\"`, so every owner-gated action becomes unreachable.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package ownable provides an ownership pattern for Gno realms, allowing\n// contracts to restrict access to privileged operations to a designated owner.\npackage ownable\n"},{"name":"errors.gno","body":"package ownable\n\nimport \"errors\"\n\nvar (\n\tErrUnauthorized   = errors.New(\"ownable: caller is not owner\")\n\tErrInvalidAddress = errors.New(\"ownable: new owner address is invalid\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/ownable/v0\"\ngno = \"0.9\"\n"},{"name":"ownable.gno","body":"package ownable\n\nimport \"chain\"\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\n// Ownable is meant to be used as a top-level object to make your contract\n// ownable OR being embedded in a Gno object to manage per-object ownership.\n// Ownable is safe to export as a top-level object.\n//\n// Authority-mutating methods (TransferOwnership, DropOwnership) take\n// (_ int, rlm realm). The caller threads its own cur; the method\n// asserts rlm.IsCurrent() and identifies the principal as\n// rlm.Previous().Address() — which must equal the current owner.\n//\n//\to.TransferOwnership(0, cur, newOwner)\n//\to.DropOwnership(0, cur)\n//\n// Read methods (OwnedBy, AssertOwnedBy) keep the bare-address shape;\n// callers extract the address themselves (e.g. cur.Previous().Address()).\ntype Ownable struct {\n\towner address\n}\n\n// NewWithAddress creates an Ownable with the given address as owner.\n// This is the only constructor — the previous New/NewWithOrigin/\n// NewWithAddressByPrevious sugar baked runtime walks and an auth-mode\n// flag into the struct; the realm using this package now picks the\n// owner address explicitly (e.g. cur.Previous().Address() after\n// verifying cur.Previous().IsUserCall() in init).\nfunc NewWithAddress(addr address) *Ownable {\n\treturn \u0026Ownable{\n\t\towner: addr,\n\t}\n}\n\n// OwnedBy reports whether addr is the current owner.\nfunc (o *Ownable) OwnedBy(addr address) bool {\n\tif o == nil {\n\t\treturn false\n\t}\n\treturn addr == o.owner\n}\n\n// AssertOwnedBy panics with ErrUnauthorized if addr is not the owner.\nfunc (o *Ownable) AssertOwnedBy(addr address) {\n\tif !o.OwnedBy(addr) {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n\n// TransferOwnership transfers ownership of the Ownable to newOwner. rlm\n// must be the caller's own captured cur (asserted via rlm.IsCurrent()).\n// The principal is rlm.Previous().Address() — the realm that crossed\n// into the caller — which must equal the current owner.\n//\n// IsCurrent + rlm.Previous() makes the principal unforgeable: an\n// attacker calling TransferOwnership on a foreign Ownable cannot supply\n// an arbitrary caller address; rlm comes from a runtime-validated\n// crossing frame.\nfunc (o *Ownable) TransferOwnership(_ int, rlm realm, newOwner address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrUnauthorized\n\t}\n\tcaller := rlm.Previous().Address()\n\tif !o.OwnedBy(caller) {\n\t\treturn ErrUnauthorized\n\t}\n\tif !newOwner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tprevOwner := o.owner\n\to.owner = newOwner\n\tchain.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", prevOwner.String(),\n\t\t\"to\", newOwner.String(),\n\t)\n\treturn nil\n}\n\n// DropOwnership removes the owner, disabling any owner-related actions.\n// rlm must be the caller's own captured cur; rlm.Previous().Address()\n// must equal the current owner.\nfunc (o *Ownable) DropOwnership(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrUnauthorized\n\t}\n\tcaller := rlm.Previous().Address()\n\tif !o.OwnedBy(caller) {\n\t\treturn ErrUnauthorized\n\t}\n\tprevOwner := o.owner\n\to.owner = \"\"\n\tchain.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", prevOwner.String(),\n\t\t\"to\", \"\",\n\t)\n\treturn nil\n}\n\n// Owner returns the owner address.\nfunc (o *Ownable) Owner() address {\n\tif o == nil {\n\t\treturn address(\"\")\n\t}\n\treturn o.owner\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"authorizable","path":"gno.land/p/nt/ownable/v0/exts/authorizable","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `authorizable` - Second authorization tier over ownable\n\nExtension of [`gno.land/p/nt/ownable/v0`](../..) that adds a second permission level on top of single-owner ownership: one **superuser** (the `ownable` owner) plus a list of **authorized** addresses. Use it for a moderator tier, an allowlist, or any \"owner, plus a set of trusted others\" pattern.\n\n## Usage\n\n```go\npackage myrealm\n\nimport (\n    \"chain/runtime\"\n\n    \"gno.land/p/nt/ownable/v0\"\n    \"gno.land/p/nt/ownable/v0/exts/authorizable\"\n)\n\n// The superuser (and first entry on the auth list) is chosen explicitly.\n// Here: the deployer, captured in init.\nvar auth *authorizable.Authorizable\n\nfunc init() {\n    caller := runtime.PreviousRealm()\n    if !caller.IsUserCall() {\n        panic(\"must be deployed by a user\")\n    }\n    auth = authorizable.New(ownable.NewWithAddress(caller.Address()))\n}\n\n// Superuser-only: add a moderator.\nfunc AddModerator(cur realm, addr address) error {\n    return auth.AddToAuthList(0, cur, addr)\n}\n\n// Gate an action to anyone on the auth list.\nfunc Moderate(cur realm) {\n    auth.AssertPreviousOnAuthList(0, cur)\n    // ... privileged work ...\n}\n```\n\n## API\n\n```go\ntype Authorizable struct {\n    *ownable.Ownable // the owner is the superuser; all Ownable methods are inherited\n    // unexported auth list\n}\n\n// New builds an Authorizable from an existing *ownable.Ownable.\n// The owner is automatically added to the auth list.\nfunc New(o *ownable.Ownable) *Authorizable\n\n// Superuser-only (previous caller must be the owner).\nfunc (a *Authorizable) AddToAuthList(_ int, rlm realm, addr address) error\nfunc (a *Authorizable) DeleteFromAuthList(_ int, rlm realm, addr address) error\n\n// Membership checks (return an error; nil means on the list).\nfunc (a *Authorizable) OnAuthList(_ int, rlm realm) error         // is the caller realm itself on the list\nfunc (a *Authorizable) PreviousOnAuthList(_ int, rlm realm) error // is the realm/user that crossed in on the list\n\n// Assert variants panic instead of returning an error.\nfunc (a Authorizable) AssertOnAuthList(_ int, rlm realm)\nfunc (a Authorizable) AssertPreviousOnAuthList(_ int, rlm realm)\n\n// Errors: ErrNotSuperuser, ErrNotInAuthList, ErrAlreadyInList\n```\n\n## Notes\n\n- Every method takes the caller's own captured `cur` as `rlm` and asserts `rlm.IsCurrent()`, blocking the designation-forgery read where a non-crossing wrapper makes the realm walk return the wrong address. The first `_ int` argument is an unused placeholder: pass `0`.\n- The superuser is authenticated by `rlm.Previous().Address()` matching the underlying `Ownable` owner, so `AddToAuthList` / `DeleteFromAuthList` succeed only when the owner is the crossing caller. Ownership transfer, renouncing, etc. come from the embedded [`Ownable`](../..).\n- `PreviousOnAuthList` / `AssertPreviousOnAuthList` are the user-facing gate: they check the address that crossed into your realm. `OnAuthList` checks the calling realm itself; use it only when a realm-to-realm caller should be listed directly.\n- The auth list is backed by a [`bptree`](../../../../bptree/v0), keyed by address string.\n"},{"name":"authorizable.gno","body":"// Package authorizable is an extension of p/nt/ownable;\n// It allows the user to instantiate an Authorizable struct, which extends\n// p/nt/ownable with a list of users that are authorized for something.\n// By using authorizable, you have a superuser (ownable), as well as another\n// authorization level, which can be used for adding moderators or similar to your realm.\npackage authorizable\n\nimport (\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Authorizable struct {\n\t*ownable.Ownable                // owner in ownable is superuser\n\tauthorized       *bptree.BPTree // chain.Addr \u003e struct{}{}\n}\n\n// New creates an Authorizable from an existing *ownable.Ownable.\n// The owner is automatically added to the auth list.\n//\n// Example construction:\n//\n//\tauthorizable.New(ownable.NewWithAddress(addr))\nfunc New(o *ownable.Ownable) *Authorizable {\n\ta := \u0026Authorizable{\n\t\tOwnable:    o,\n\t\tauthorized: bptree.NewBPTree32(),\n\t}\n\n\t// Add owner to auth list\n\ta.authorized.Set(a.Owner().String(), struct{}{})\n\treturn a\n}\n\n// AddToAuthList adds addr to the auth list. rlm must be the caller's\n// own captured cur; rlm.Previous().Address() must equal the superuser\n// (the underlying Ownable's owner).\nfunc (a *Authorizable) AddToAuthList(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotSuperuser\n\t}\n\tif !a.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ErrNotSuperuser\n\t}\n\treturn a.addToAuthList(addr)\n}\n\nfunc (a *Authorizable) addToAuthList(addr address) error {\n\tif a.authorized.Has(addr.String()) {\n\t\treturn ErrAlreadyInList\n\t}\n\n\ta.authorized.Set(addr.String(), struct{}{})\n\n\treturn nil\n}\n\n// DeleteFromAuthList removes addr from the auth list. rlm must be the\n// caller's own captured cur; rlm.Previous().Address() must equal the\n// superuser (the underlying Ownable's owner).\nfunc (a *Authorizable) DeleteFromAuthList(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotSuperuser\n\t}\n\tif !a.OwnedBy(rlm.Previous().Address()) {\n\t\treturn ErrNotSuperuser\n\t}\n\treturn a.deleteFromAuthList(addr)\n}\n\nfunc (a *Authorizable) deleteFromAuthList(addr address) error {\n\tif !a.authorized.Has(addr.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\n\tif _, removed := a.authorized.Remove(addr.String()); !removed {\n\t\tstr := ufmt.Sprintf(\"authorizable: could not remove %s from auth list\", addr.String())\n\t\tpanic(str)\n\t}\n\n\treturn nil\n}\n\n// OnAuthList reports whether rlm.Address() is on the auth list. rlm\n// must be the caller's own captured cur (asserted via rlm.IsCurrent()).\n// Pre-migration shape used unsafe.CurrentRealm().Address() — vulnerable\n// to the .Title()-class read where a non-crossing wrapper made the walk\n// return the wrong realm. Explicit rlm closes that.\nfunc (a *Authorizable) OnAuthList(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotInAuthList\n\t}\n\treturn a.onAuthList(rlm.Address())\n}\n\n// PreviousOnAuthList reports whether rlm.Previous().Address() — the\n// realm that crossed into the caller — is on the auth list. Same rlm\n// contract as OnAuthList.\nfunc (a *Authorizable) PreviousOnAuthList(_ int, rlm realm) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrNotInAuthList\n\t}\n\treturn a.onAuthList(rlm.Previous().Address())\n}\n\nfunc (a *Authorizable) onAuthList(caller address) error {\n\tif !a.authorized.Has(caller.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\treturn nil\n}\n\nfunc (a Authorizable) AssertOnAuthList(_ int, rlm realm) {\n\tif err := a.OnAuthList(0, rlm); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (a Authorizable) AssertPreviousOnAuthList(_ int, rlm realm) {\n\tif err := a.PreviousOnAuthList(0, rlm); err != nil {\n\t\tpanic(err)\n\t}\n}\n"},{"name":"errors.gno","body":"package authorizable\n\nimport \"errors\"\n\nvar (\n\tErrNotInAuthList = errors.New(\"authorizable: caller is not in authorized list\")\n\tErrNotSuperuser  = errors.New(\"authorizable: caller is not superuser\")\n\tErrAlreadyInList = errors.New(\"authorizable: address is already in authorized list\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/ownable/v0/exts/authorizable\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc20","path":"gno.land/p/demo/tokens/grc20","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tokens/grc20\"\ngno = \"0.9\"\n"},{"name":"mock.gno","body":"package grc20\n\n// XXX: func Mock(t *Token)\n"},{"name":"tellers.gno","body":"package grc20\n\nimport (\n\t\"chain\"\n)\n\n// CallerTeller returns a GRC20 compatible teller that, at each write call,\n// resolves the caller as rlm.Previous() — the realm that crossed into the\n// caller. rlm must be the caller's own captured cur (asserted via\n// rlm.IsCurrent() inside the Teller methods).\nfunc (tok *Token) CallerTeller() Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func(_ int, rlm realm) address {\n\t\t\treturn rlm.Previous().Address()\n\t\t},\n\t\tToken: tok,\n\t}\n}\n\n// ReadonlyTeller is a GRC20 compatible teller that panics for any write operation.\nfunc (tok *Token) ReadonlyTeller() Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: nil,\n\t\tToken:     tok,\n\t}\n}\n\n// RealmTeller returns a GRC20 compatible teller that will store the\n// caller realm permanently. Calling anything through this teller will\n// result in allowance or balance changes for the realm that initialized the teller.\n// The initializer of this teller should usually never share the resulting Teller from\n// this method except maybe for advanced delegation flows such as a DAO treasury\n// management.\n//\n// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()).\n// The address is frozen eagerly at construction.\nfunc (tok *Token) RealmTeller(_ int, rlm realm) Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrSpoofedRealm)\n\t}\n\n\tcaller := rlm.Address()\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func(_ int, _ realm) address {\n\t\t\treturn caller\n\t\t},\n\t\tToken: tok,\n\t}\n}\n\n// RealmSubTeller is like RealmTeller but uses the provided slug to derive a\n// subaccount.\n//\n// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()).\n// The subaccount address is frozen eagerly at construction.\nfunc (tok *Token) RealmSubTeller(_ int, rlm realm, slug string) Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrSpoofedRealm)\n\t}\n\n\taccount := accountSlugAddr(rlm.Address(), slug)\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func(_ int, _ realm) address {\n\t\t\treturn account\n\t\t},\n\t\tToken: tok,\n\t}\n}\n\n// ImpersonateTeller returns a GRC20 compatible teller that impersonates as a\n// specified address. This allows operations to be performed as if they were\n// executed by the given address, enabling the caller to manipulate tokens on\n// behalf of that address.\n//\n// It is particularly useful in scenarios where a contract needs to perform\n// actions on behalf of a user or another account, without exposing the\n// underlying logic or requiring direct access to the user's account. The\n// returned teller will use the provided address for all operations, effectively\n// masking the original caller.\n//\n// This method should be used with caution, as it allows for potentially\n// sensitive operations to be performed under the guise of another address.\nfunc (ledger *PrivateLedger) ImpersonateTeller(addr address) Teller {\n\tif ledger == nil {\n\t\tpanic(\"Ledger cannot be nil\")\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func(_ int, _ realm) address {\n\t\t\treturn addr\n\t\t},\n\t\tToken: ledger.token,\n\t}\n}\n\n// generic tellers methods.\n//\n\nfunc (ft *fnTeller) Transfer(_ int, rlm realm, to address, amount int64) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tcaller := ft.accountFn(0, rlm)\n\treturn ft.Token.ledger.Transfer(caller, to, amount)\n}\n\nfunc (ft *fnTeller) Approve(_ int, rlm realm, spender address, amount int64) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tcaller := ft.accountFn(0, rlm)\n\treturn ft.Token.ledger.Approve(caller, spender, amount)\n}\n\nfunc (ft *fnTeller) TransferFrom(_ int, rlm realm, owner, to address, amount int64) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tspender := ft.accountFn(0, rlm)\n\treturn ft.Token.ledger.TransferFrom(owner, spender, to, amount)\n}\n\n// helpers\n//\n\n// accountSlugAddr returns the address derived from the specified address and slug.\nfunc accountSlugAddr(addr address, slug string) address {\n\t// XXX: use a new `std.XXX` call for this.\n\tif slug == \"\" {\n\t\treturn addr\n\t}\n\tkey := addr.String() + \"/\" + slug\n\treturn chain.PackageAddress(key) // temporarily using this helper\n}\n"},{"name":"token.gno","body":"package grc20\n\nimport (\n\t\"chain\"\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewToken creates a Token whose origRealm is bound to the calling realm.\n// rlm must be the caller's own captured cur (asserted via rlm.IsCurrent()),\n// and rlm.PkgPath() — the calling realm itself — becomes the Token's\n// origRealm. Token.ID() returns origRealm + \".\" + symbol + \".\" + id.\n//\n// Because IsCurrent runtime-validates that rlm came from the live\n// crossing frame, origRealm is unforgeable: an external realm cannot\n// fabricate a Token claiming to belong to a different package.\n//\n// Realms that create multiple tokens should allocate id from one persistent\n// seqid.ID, shared by every creation path, to avoid conflicting identifiers:\n//\n//\tvar nextTokenID seqid.ID\n//\tToken, ledger := grc20.NewToken(\"Foo\", \"FOO\", 4, nextTokenID.Next(), cur)\n//\n// A realm that creates only a single token can pass 0 directly, since no\n// other token of that realm can collide with it.\n//\n// If the Token should be discoverable, follow up with\n// grc20reg.Register(cross(cur), Token, slug). The registry key is Token.ID().\n//\n// Every successful call emits a NewToken event carrying the resulting\n// Token.ID(). Because Token's fields are unexported, NewToken is the only way a\n// Token can come into existence, so this event makes token creation fully\n// observable: an indexer that sees the same Token.ID() announced twice knows the\n// realm built two independent ledgers behind one identifier, and that every\n// later Mint/Burn/Transfer/Approval carrying that id is ambiguous. Such a realm\n// is emitting untrustworthy events and should be flagged or ignored wholesale.\nfunc NewToken(name, symbol string, decimals int, id seqid.ID, rlm realm) (*Token, *PrivateLedger) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrSpoofedRealm)\n\t}\n\tpkgPath := rlm.PkgPath()\n\tif pkgPath == \"\" {\n\t\tpanic(ErrNotRealm)\n\t}\n\tif !validName(name) {\n\t\tpanic(ErrInvalidName)\n\t}\n\tif !validSymbol(symbol) {\n\t\tpanic(ErrInvalidSymbol)\n\t}\n\tif decimals \u003c 0 || decimals \u003e MaxDecimals {\n\t\tpanic(ErrInvalidDecimals)\n\t}\n\tledger := \u0026PrivateLedger{}\n\ttoken := \u0026Token{\n\t\tid:       pkgPath + \".\" + symbol + \".\" + id.String(),\n\t\tname:     name,\n\t\tsymbol:   symbol,\n\t\tdecimals: decimals,\n\t\tledger:   ledger,\n\t}\n\tledger.token = token\n\n\tchain.Emit(\n\t\tNewTokenEvent,\n\t\t\"token\", token.id,\n\t\t\"name\", name,\n\t\t\"symbol\", symbol,\n\t\t\"decimals\", strconv.Itoa(decimals),\n\t)\n\n\treturn token, ledger\n}\n\n// validName reports whether name is a valid display name: non-empty,\n// within MaxNameLen, and contains no control characters (any rune\n// below 0x20 or 0x7f). Permits Unicode letters, digits, punctuation,\n// and spaces — name is purely a display field.\nfunc validName(name string) bool {\n\tif name == \"\" || len(name) \u003e MaxNameLen {\n\t\treturn false\n\t}\n\tfor _, c := range name {\n\t\tif c \u003c 0x20 || c == 0x7f {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// validSymbol reports whether s is valid slug-compatible metadata: non-empty,\n// within MaxSymbolLen, and consists only of [A-Za-z0-9_-].\nfunc validSymbol(s string) bool {\n\tif s == \"\" || len(s) \u003e MaxSymbolLen {\n\t\treturn false\n\t}\n\tfor _, c := range s {\n\t\tif !isAlnum(c) \u0026\u0026 c != '_' \u0026\u0026 c != '-' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isAlnum(c rune) bool {\n\treturn (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') || (c \u003e= '0' \u0026\u0026 c \u003c= '9')\n}\n\n// GetName returns the name of the token.\nfunc (tok Token) GetName() string { return tok.name }\n\n// GetSymbol returns the symbol of the token.\nfunc (tok Token) GetSymbol() string { return tok.symbol }\n\n// GetDecimals returns the number of decimals used to get the token's precision.\nfunc (tok Token) GetDecimals() int { return tok.decimals }\n\n// TotalSupply returns the total supply of the token.\nfunc (tok Token) TotalSupply() int64 { return tok.ledger.totalSupply }\n\n// KnownAccounts returns the number of known accounts in the bank.\nfunc (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() }\n\n// ID returns the Identifier of the token.\n// It is composed of the original realm, the symbol, and the provided id.\nfunc (tok *Token) ID() string {\n\treturn tok.id\n}\n\n// HasAddr checks if the specified address is a known account in the bank.\nfunc (tok Token) HasAddr(addr address) bool {\n\treturn tok.ledger.hasAddr(addr)\n}\n\n// BalanceOf returns the balance of the specified address.\nfunc (tok Token) BalanceOf(addr address) int64 {\n\treturn tok.ledger.balanceOf(addr)\n}\n\n// Allowance returns the allowance of the specified owner and spender.\nfunc (tok Token) Allowance(owner, spender address) int64 {\n\treturn tok.ledger.allowance(owner, spender)\n}\n\nfunc (tok Token) RenderHome() string {\n\tstr := \"\"\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", tok.name, tok.symbol)\n\tstr += ufmt.Sprintf(\"* **Decimals**: %d\\n\", tok.decimals)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", tok.ledger.totalSupply)\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", tok.KnownAccounts())\n\treturn str\n}\n\n// SpendAllowance decreases the allowance of the specified owner and spender.\nfunc (led *PrivateLedger) SpendAllowance(owner, spender address, amount int64) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\t// do nothing\n\tif amount == 0 {\n\t\treturn nil\n\t}\n\n\tcurrentAllowance := led.allowance(owner, spender)\n\tif currentAllowance \u003c amount {\n\t\treturn ErrInsufficientAllowance\n\t}\n\n\tkey := allowanceKey(owner, spender)\n\tnewAllowance := overflow.Sub64p(currentAllowance, amount)\n\n\tif newAllowance == 0 {\n\t\tled.allowances.Remove(key)\n\t} else {\n\t\tled.allowances.Set(key, newAllowance)\n\t}\n\n\treturn nil\n}\n\n// Transfer transfers tokens from the specified from address to the specified to address.\nfunc (led *PrivateLedger) Transfer(from, to address, amount int64) error {\n\tif !from.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tvar (\n\t\ttoBalance   = led.balanceOf(to)\n\t\tfromBalance = led.balanceOf(from)\n\t)\n\n\tif fromBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tvar (\n\t\tnewToBalance   = overflow.Add64p(toBalance, amount)\n\t\tnewFromBalance = overflow.Sub64p(fromBalance, amount)\n\t)\n\n\tled.balances.Set(string(to), newToBalance)\n\n\tif newFromBalance == 0 {\n\t\tled.balances.Remove(string(from))\n\t} else {\n\t\tled.balances.Set(string(from), newFromBalance)\n\t}\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"from\", from.String(),\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// TransferFrom transfers tokens from the specified owner to the specified to address.\n// It first checks if the owner has sufficient balance and then decreases the allowance.\nfunc (led *PrivateLedger) TransferFrom(owner, spender, to address, amount int64) error {\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tif !owner.IsValid() || !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif led.balanceOf(owner) \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\t// The check above guarantees that Transfer will succeed, ensuring\n\t// atomicity for the subsequent operations.\n\tif err := led.SpendAllowance(owner, spender, amount); err != nil {\n\t\treturn err\n\t}\n\n\tif err := led.Transfer(owner, to, amount); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Approve sets the allowance of the specified owner and spender.\nfunc (led *PrivateLedger) Approve(owner, spender address, amount int64) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tled.allowances.Set(allowanceKey(owner, spender), amount)\n\n\tchain.Emit(\n\t\tApprovalEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"owner\", string(owner),\n\t\t\"spender\", string(spender),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// Mint increases the total supply of the token and adds the specified amount to the specified address.\nfunc (led *PrivateLedger) Mint(addr address, amount int64) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\t// limit amount to MaxInt64 - totalSupply\n\tif amount \u003e overflow.Sub64p(math.MaxInt64, led.totalSupply) {\n\t\treturn ErrMintOverflow\n\t}\n\n\tled.totalSupply += amount\n\tcurrentBalance := led.balanceOf(addr)\n\tnewBalance := overflow.Add64p(currentBalance, amount)\n\n\tled.balances.Set(string(addr), newBalance)\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"from\", \"\",\n\t\t\"to\", string(addr),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// Burn decreases the total supply of the token and subtracts the specified amount from the specified address.\nfunc (led *PrivateLedger) Burn(addr address, amount int64) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tcurrentBalance := led.balanceOf(addr)\n\tif currentBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tled.totalSupply = overflow.Sub64p(led.totalSupply, amount)\n\tnewBalance := overflow.Sub64p(currentBalance, amount)\n\n\tif newBalance == 0 {\n\t\tled.balances.Remove(string(addr))\n\t} else {\n\t\tled.balances.Set(string(addr), newBalance)\n\t}\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"from\", string(addr),\n\t\t\"to\", \"\",\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// hasAddr checks if the specified address is a known account in the ledger.\nfunc (led PrivateLedger) hasAddr(addr address) bool {\n\treturn led.balances.Has(addr.String())\n}\n\n// balanceOf returns the balance of the specified address.\nfunc (led PrivateLedger) balanceOf(addr address) int64 {\n\tbalance := led.balances.Get(addr.String())\n\tif balance == nil {\n\t\treturn 0\n\t}\n\treturn balance.(int64)\n}\n\n// allowance returns the allowance of the specified owner and spender.\nfunc (led PrivateLedger) allowance(owner, spender address) int64 {\n\tallowance := led.allowances.Get(allowanceKey(owner, spender))\n\tif allowance == nil {\n\t\treturn 0\n\t}\n\treturn allowance.(int64)\n}\n\n// allowanceKey returns the key for the allowance of the specified owner and spender.\nfunc allowanceKey(owner, spender address) string {\n\treturn owner.String() + \":\" + spender.String()\n}\n"},{"name":"types.gno","body":"package grc20\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Teller interface defines the methods that a GRC20 token must implement. It\n// extends the TokenMetadata interface to include methods for managing token\n// transfers, allowances, and querying balances.\n//\n// The Teller interface is designed to ensure that any token adhering to this\n// standard provides a consistent API for interacting with fungible tokens.\n//\n// SECURITY: Transfer/Approve/TransferFrom take (_ int, rlm realm, ...), so\n// handing a Teller value to untrusted code yields a capability token to\n// whatever Transfer/Approve/TransferFrom impl that code dispatches into.\n// Any /p/ or /r/ function that accepts a Teller as a parameter from external\n// callers MUST type-assert against the canonical concrete type (*fnTeller)\n// via IsCanonicalTeller and reject otherwise. An unexported-marker \"seal\"\n// does NOT defend against this — see\n// p/test/seal/filetests/z_seal_iface_embedding_filetest.gno\n// for the realistic embedding-bypass attack. Reference impl for the\n// canonical-allowlist pattern: p/jaekwon/allowancesender.\ntype Teller interface {\n\t// Returns the name of the token.\n\tGetName() string\n\n\t// Returns the symbol of the token, usually a shorter version of the\n\t// name.\n\tGetSymbol() string\n\n\t// Returns the decimals places of the token.\n\tGetDecimals() int\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() int64\n\n\t// Returns the amount of tokens owned by `account`.\n\tBalanceOf(account address) int64\n\n\t// Moves `amount` tokens from the caller's account to `to`. rlm must\n\t// be the caller's own captured cur — verified via rlm.IsCurrent().\n\t//\n\t// Returns an error if the operation failed.\n\tTransfer(_ int, rlm realm, to address, amount int64) error\n\n\t// Returns the remaining number of tokens that `spender` will be\n\t// allowed to spend on behalf of `owner` through {transferFrom}. This is\n\t// zero by default.\n\t//\n\t// This value changes when {approve} or {transferFrom} are called.\n\tAllowance(owner, spender address) int64\n\n\t// Sets `amount` as the allowance of `spender` over the caller's tokens.\n\t//\n\t// Returns an error if the operation failed.\n\t//\n\t// IMPORTANT: Beware that changing an allowance with this method brings\n\t// the risk that someone may use both the old and the new allowance by\n\t// unfortunate transaction ordering. One possible solution to mitigate\n\t// this race condition is to first reduce the spender's allowance to 0\n\t// and set the desired value afterwards:\n\t// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n\tApprove(_ int, rlm realm, spender address, amount int64) error\n\n\t// Moves `amount` tokens from `from` to `to` using the\n\t// allowance mechanism. `amount` is then deducted from the caller's\n\t// allowance.\n\t//\n\t// Returns an error if the operation failed.\n\tTransferFrom(_ int, rlm realm, from, to address, amount int64) error\n}\n\n// Token represents a fungible token with an ID, name, symbol, and a certain\n// number of decimal places. It maintains a ledger for tracking balances and\n// allowances of addresses.\n//\n// The Token struct provides methods for retrieving token metadata, such as the\n// name, symbol, and decimals, as well as methods for interacting with the\n// ledger, including checking balances and allowances.\ntype Token struct {\n\t// Identifier precomputed in NewToken to make ID() a cheap field read.\n\tid string\n\t// Name of the token (e.g., \"Dummy Token\").\n\tname string\n\t// Symbol of the token (e.g., \"DUMMY\").\n\tsymbol string\n\t// Number of decimal places used for the token's precision.\n\tdecimals int\n\t// Pointer to the PrivateLedger that manages balances and allowances.\n\tledger *PrivateLedger\n}\n\n// PrivateLedger is a struct that holds the balances and allowances for the\n// token. It provides administrative functions for minting, burning,\n// transferring tokens, and managing allowances.\n//\n// The PrivateLedger is not safe to expose publicly, as it contains sensitive\n// information regarding token balances and allowances, and allows direct,\n// unrestricted access to all administrative functions.\ntype PrivateLedger struct {\n\t// Total supply of the token managed by this ledger.\n\ttotalSupply int64\n\t// chain.Address -\u003e int64\n\tbalances avl.Tree\n\t// owner.(chain.Address)+\":\"+spender.(chain.Address)) -\u003e int64\n\tallowances avl.Tree\n\t// Pointer to the associated Token struct\n\ttoken *Token\n}\n\nvar (\n\tErrInsufficientBalance   = errors.New(\"insufficient balance\")\n\tErrInsufficientAllowance = errors.New(\"insufficient allowance\")\n\tErrInvalidAddress        = errors.New(\"invalid address\")\n\tErrCannotTransferToSelf  = errors.New(\"cannot send transfer to self\")\n\tErrReadonly              = errors.New(\"banker is readonly\")\n\tErrRestrictedTokenOwner  = errors.New(\"restricted to bank owner\")\n\tErrMintOverflow          = errors.New(\"mint overflow\")\n\tErrInvalidAmount         = errors.New(\"invalid amount\")\n\tErrSpoofedRealm          = errors.New(\"rlm does not match the current crossing frame\")\n\tErrNotRealm              = errors.New(\"rlm must be a realm (got EOA/origin)\")\n\tErrInvalidName           = errors.New(\"invalid token name (empty, too long, or contains control chars)\")\n\tErrInvalidSymbol         = errors.New(\"invalid token symbol (empty, too long, or contains chars outside [A-Za-z0-9_-])\")\n\tErrInvalidDecimals       = errors.New(\"invalid decimals (must be 0..18)\")\n)\n\n// Construction limits. Symbol is restricted to the same charset as\n// grc20reg.validateSlug because it is included in Token.ID(), which is\n// emitted in events and frequently used as a registry slug; banning `.` `/`\n// and whitespace here prevents downstream parsers from being fooled by\n// ambiguous IDs. Name is for display only and allows any valid UTF-8 except\n// control characters.\nconst (\n\tMaxNameLen   = 64\n\tMaxSymbolLen = 11\n\tMaxDecimals  = 18\n)\n\nconst (\n\tNewTokenEvent = \"NewToken\"\n\tMintEvent     = \"Mint\"\n\tBurnEvent     = \"Burn\"\n\tTransferEvent = \"Transfer\"\n\tApprovalEvent = \"Approval\"\n)\n\ntype fnTeller struct {\n\taccountFn func(_ int, rlm realm) address\n\t*Token\n}\n\nvar _ Teller = (*fnTeller)(nil)\n\n// IsCanonicalTeller reports whether t is the canonical *fnTeller produced by\n// Token.CallerTeller / RealmTeller / RealmSubTeller / ReadonlyTeller /\n// ImpersonateTeller. Use this at any public entry point that accepts a\n// Teller from an external caller before invoking its methods.\n//\n// Foreign types — including embedding-based wrappers like\n// `type Evil struct { grc20.Teller }` — are rejected because type\n// assertions are nominal: *Evil is not *fnTeller, regardless of method\n// promotion. This is the reliable defense; the unexported-marker \"seal\"\n// pattern is bypassable via embedding (see\n// p/test/seal/filetests/z_seal_iface_embedding_filetest.gno).\n//\n// Mirrors the precedent of chain/banker.IsCanonical and\n// p/jaekwon/allowancesender's canonical-impl check.\nfunc IsCanonicalTeller(t Teller) bool {\n\t_, ok := t.(*fnTeller)\n\treturn ok\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"sanitize","path":"gno.land/p/nt/markdown/sanitize/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `sanitize` - Markdown input sanitizers\n\nInput-cleaning primitives and safe-emit builders, one per markdown lexical slot. Wrap a user-supplied string with the matching helper before flowing it into rendered markdown, so user content cannot break out of its slot or inject new top-level structure (a heading, table, code fence, link-reference definition, HTML block, or invisible bidi/zero-width spoof).\n\n## Usage\n\n```go\nimport \"gno.land/p/nt/markdown/sanitize/v0\"\n\nout := \"# \" + sanitize.InlineText(userTitle) + \"\\n\\n\" +\n    sanitize.Block(userBody)\nout += sanitize.Blockquote(userQuote)\nout += sanitize.LanguageCodeBlock(realmLang, userCode)\n```\n\n## Two rules\n\n1. **Wrap once.** Most helpers are *not* idempotent: a second pass re-escapes the bytes the first added (`\\*` becomes `\\\\\\*`, `\u0026amp;` becomes `\u0026amp;amp;`, a fenced block gets re-fenced). Wrap each user-derived string with at most one `sanitize.*` call. If a builder package (e.g. `p/moul/md`) already sanitizes an argument, pass the raw input, do not pre-wrap.\n2. **Right helper per slot.** Match the helper to the slot the content lands in.\n\n## Picking the right helper\n\n| Slot | Helper |\n|---|---|\n| `[text](url)`, `# Heading`, `**bold**`, `![alt]`, alert title | `InlineText` |\n| Multi-paragraph body (paragraph-only) | `Block` |\n| Multi-paragraph body with rich structure (headings, lists, tables) | `BlockRich` |\n| Multi-line blockquote | `Blockquote` / `BlockquoteRich` |\n| `[text](url \"title\")` | `LinkTitle` |\n| Table cell | `TableCell` |\n| Inside an HTML tag/attribute (`\u003cgno-card caption=\"X\"\u003e`) | `HTMLEscape` |\n| Any link URL / image src | `URL` / `ImageURL` |\n| Inline / fenced code, with or without a language tag | `InlineCode` / `CodeBlock` / `LanguageCodeBlock` |\n| Footnote body / link-reference definition | `FootnoteDefinition` / `LinkReferenceDefinition` |\n| Validate a handle / bech32 address / label / language / nest prefix | `UserName` / `BechString` / `FootnoteLabel` / `LanguageName` / `NestedPrefix` |\n\n## Escapers vs validators\n\n- **Escapers** always return a transformed, safe string and never reject: any input is acceptable because the transformation makes it safe.\n- **Validators** (`UserName`, `BechString`, `FootnoteLabel`, `LanguageName`, `NestedPrefix`) return the cleaned input verbatim on accept, or `\"\"` on reject. They never half-process, so `\"\"` unambiguously means rejected (or empty input).\n\n## `Block` vs `BlockRich`\n\nBoth run identical realm-binding defenses; they differ in what user structure survives.\n\n- **`Block`** — paragraph-shaped only. Escapes `#`, `\u003e`, list markers, thematic breaks, and setext underlines. Use for leaf slots and any content that must not visually impersonate realm chrome.\n- **`BlockRich`** — preserves user headings, lists, quotes, and tables. Use for content the realm intends to render with full block structure, typically inside a sandbox container (`\u003cgno-card\u003e`, [`\u003cgno-foreign\u003e`](../../foreign/v0)). Inner-heading visual containment is the realm's CSS responsibility.\n\nDo not compose the two in either direction; pick one at the right level.\n\n## API\n\nEscapers (always return a safe, transformed string; never reject):\n\n```go\nfunc InlineText(s string) string\nfunc Block(s string) string\nfunc BlockRich(s string) string\nfunc Blockquote(text string) string\nfunc BlockquoteRich(text string) string\nfunc LinkTitle(s string) string\nfunc TableCell(s string) string\nfunc HTMLEscape(s string) string\nfunc URL(s string) string\nfunc ImageURL(s string) string\nfunc InlineCode(content string) string\nfunc CodeBlock(content string) string\nfunc LanguageCodeBlock(language, content string) string\nfunc CodeFence(content string, minCount int) string // raw fence builder for custom emitters\nfunc FootnoteDefinition(name, text string) string\nfunc LinkReferenceDefinition(label, url, title string) string\n```\n\nValidators (return the cleaned input verbatim, or `\"\"` on reject):\n\n```go\nfunc UserName(s string) string\nfunc BechString(s, prefix string) string\nfunc FootnoteLabel(s string) string\nfunc LanguageName(s string) string\nfunc NestedPrefix(s string) string\n```\n\nLow-level normalizers (rarely needed directly; the helpers above call them):\n\n```go\nfunc StripBidiAndZeroWidth(s string) string\nfunc NormalizeBreaks(s string) string\n```\n\n## Threat model\n\nHelpers defend against bidi/zero-width injection, line-ending homoglyphs, markdown-structure injection, CommonMark HTML-block absorption (types 1-5 that do not close on a blank line), footnote / link-reference namespace pollution, URL scheme abuse (`javascript:`, `data:text/html`, protocol-relative), unclosed code-fence leakage, and table-alignment drift.\n\nOut of scope: no state, no URL reputation, no CSS containment, and no structural sandboxing of opaque foreign blobs (use [`foreign`](../../foreign/v0) for that).\n\n## Notes\n\n- Every helper is a pure function, panic-free for any string input, and runs in `O(len(input))` with bounded allocation.\n- Every text-shaped helper strips bidi/zero-width characters (`Block` and `BlockRich` normalize line breaks first, then strip), so displayed text always matches stored bytes.\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/markdown/sanitize/v0\"\ngno = \"0.9\"\n"},{"name":"sanitize.gno","body":"// Package sanitize provides input-cleaning primitives and safe-emit\n// builders for each markdown lexical slot. Realm authors wrap user-\n// supplied strings with these helpers before flowing them into rendered\n// markdown output. Each helper targets one specific slot (link text,\n// heading text, URL href, table cell, HTML attribute, fenced code block,\n// blockquote, footnote definition, link-reference definition, etc.) and\n// neutralizes the bytes that would otherwise let user content break out\n// of that slot or inject new top-level structure.\n//\n// Pick the right helper from the table under \"Picking the right helper\"\n// below, then wrap each user-supplied argument exactly once at the call\n// site (see \"The audit rule\").\n//\n// # Wrap once\n//\n// Most escapers and safe-emit builders in this package are NOT\n// idempotent — applying them twice re-escapes bytes the first pass\n// added (`\\*` becomes `\\\\\\*`, `\u0026amp;` becomes `\u0026amp;amp;`, a fenced\n// block gets re-fenced). Wrap each user-derived string with at most\n// one sanitize.* call. Block and BlockRich are exceptions —\n// idempotent by design — but the at-most-once rule is still the\n// safest default. See the \"Idempotence classes\" enumeration below\n// for the full breakdown.\n//\n// Some markdown-builder packages (e.g. p/moul/md) sanitize the args of\n// specific helpers internally — see each builder's package doc for the\n// per-helper contract. If the builder sanitizes for you, pass the raw\n// user input; if it doesn't, wrap the input with the right sanitize.*\n// helper at the call site.\n//\n// # Picking the right helper\n//\n// Match the helper to the slot the user content lands in:\n//\n//\tslot                                       helper\n//\t-------------------------------------------------------------\n//\t[text](url)                                InlineText (text)\n//\t# Heading text                             InlineText\n//\t**bold** _italic_                          InlineText\n//\t![alt](src)                                InlineText (alt)\n//\t\u003e [!NOTE] one-line title                   InlineText\n//\tmulti-paragraph post body                  Block\n//\tmulti-paragraph post body w/ rich block    BlockRich\n//\t  structure (headings, lists, tables, etc.)\n//\tmulti-line blockquote (`\u003e ` prefixed)      Blockquote\n//\tmulti-line blockquote w/ rich block body   BlockquoteRich\n//\t[text](url \"title\")                        LinkTitle (title)\n//\t| cell |                                   TableCell\n//\t\u003cgno-card caption=\"X\"\u003e                     HTMLEscape\n//\t\u003ch5\u003eX\u003c/h5\u003e                                 HTMLEscape\n//\tany URL going into ](X)                    URL\n//\tany image src going into (X)               ImageURL\n//\t`inline code` inside running prose         InlineCode\n//\tmulti-line fenced code block               CodeBlock\n//\tmulti-line fenced code with language tag   LanguageCodeBlock\n//\t[^name]: footnote body                     FootnoteDefinition\n//\t[label]: url \"title\" reference def         LinkReferenceDefinition\n//\tr/sys/users handle                         UserName       (validator)\n//\tg1.../gpub1... etc.                        BechString     (validator)\n//\tfootnote / LRD label / {#id} anchor name   FootnoteLabel  (validator)\n//\tfenced-code language tag                   LanguageName   (validator)\n//\tprefix arg to md.Nested                    NestedPrefix   (validator)\n//\n// # Invariants\n//\n// All helpers in this package are panic-free for any string input and\n// run in O(len(input)) time with bounded allocation.\n//\n// Idempotence classes:\n//\n//\tIdempotent (calling twice == calling once):\n//\t  StripBidiAndZeroWidth, NormalizeBreaks\n//\t  UserName, BechString, FootnoteLabel, LanguageName, NestedPrefix\n//\t  URL, ImageURL              (accept→identity; reject→\"\")\n//\t  Block                      (bracket walker treats \\[/\\] as ordinary;\n//\t                              line-leader escapes don't re-fire on\n//\t                              already-escaped `\\#` etc.)\n//\t  BlockRich                  (TrimLeft/TrimRight + \"\\n\\n\" wrap is stable)\n//\n//\tNOT idempotent — never wrap an already-sanitized string:\n//\t  InlineText, LinkTitle, TableCell   (re-escape backslashes)\n//\t  HTMLEscape                         (re-escapes `\u0026` → `\u0026amp;`)\n//\t  Blockquote, BlockquoteRich         (re-prefixes `\u003e `, nesting the quote each pass)\n//\t  InlineCode, CodeBlock,\n//\t  LanguageCodeBlock                  (wrap with a fence — calling twice double-wraps)\n//\t  FootnoteDefinition,\n//\t  LinkReferenceDefinition            (compose Block/InlineText/URL internally —\n//\t                                      passing already-sanitized strings double-escapes)\n//\n//\tCodeFence is pure: same inputs always give the same output.\n//\n// Validators (UserName / BechString / FootnoteLabel / LanguageName /\n// NestedPrefix) return either the cleaned input verbatim or \"\". They\n// never partially-sanitize: if the input doesn't match the slot's\n// charset/shape, the answer is rejection.\n//\n// # Composition rules\n//\n// Direct sanitize use (when emitting markdown without a builder package):\n//\n//\tout := \"# \" + sanitize.InlineText(userTitle) + \"\\n\\n\" +\n//\t       sanitize.Block(userBody)\n//\tout += sanitize.Blockquote(userQuote)\n//\tout += sanitize.LanguageCodeBlock(realmLang, userCode)\n//\n// Use with a builder package (e.g. p/moul/md): pass raw user input to\n// the builder helpers that sanitize internally — do NOT pre-wrap with\n// sanitize.*, or the input gets double-escaped (escapers are not\n// idempotent). See the builder's package doc for the per-helper\n// contract. For example, with p/moul/md:\n//\n//\tmd.Blockquote(userProse)                 // good — md.Blockquote sanitizes\n//\tmd.LanguageCodeBlock(realmLang, userCode) // good — sanitizes both args\n//\tmd.Link(userText, userURL)               // good — sanitizes both slots\n//\n//\tmd.Blockquote(sanitize.Block(userProse))         // BAD: double-wrap\n//\tmd.Link(sanitize.InlineText(t), sanitize.URL(u)) // BAD: double-wrap\n//\n// Wrong (across all callers):\n//\n//\tsanitize.InlineText(sanitize.InlineText(s))   double-wrap (re-escape)\n//\tsanitize.TableCell(sanitize.InlineText(s))    TableCell already calls InlineText\n//\tsanitize.URL(sanitize.InlineText(href))       inline-escape backslash-escapes `.` `-` `_`\n//\t                                              inside the URL, corrupting the host/path\n//\tsanitize.Blockquote(sanitize.Blockquote(s))   double-wrap — outer would escape the\n//\t                                              inner `\u003e ` prefixes\n//\tsanitize.Block(sanitize.BlockRich(s))         double-sanitize — strict Block re-escapes\n//\t                                              the markers BlockRich preserved (headings,\n//\t                                              lists, tables); BlockRich's rich structure\n//\t                                              renders as literal text after Block escapes\n//\t                                              its line-leaders\n//\tsanitize.BlockRich(sanitize.Block(s))         pointless double-sanitize — Block already\n//\t                                              escaped every line-leader to `\\#`/`\\\u003e`/etc.;\n//\t                                              BlockRich preserves the backslash escapes\n//\t                                              as visible artifacts in user prose\n//\tsanitize.Blockquote(sanitize.BlockRich(s))    double-sanitize — Blockquote's Block step\n//\t                                              re-escapes the markers BlockRich preserved\n//\tsanitize.BlockRich(sanitize.Blockquote(s))    nonsense — Blockquote already line-prefixed\n//\t                                              with `\u003e `; BlockRich expects raw user content\n//\tsanitize.BlockquoteRich(sanitize.BlockRich(s)) double-wrap — Rich + Rich nests twice\n//\tsanitize.BlockRich(sanitize.TableCell(s))     wrong slot — use TableCell for cell content,\n//\t                                              BlockRich for multi-paragraph block content\n//\tsanitize.TableCell(multiParagraphProse)       newlines fold to space silently; use a\n//\t                                              non-table layout for multi-paragraph text\n//\n// # Threat model\n//\n// Sanitizers in this package defend against:\n//\n//   - bidi/zero-width injection: invisible characters that make\n//     displayed text disagree with stored bytes (e.g. an address `g1abc...`\n//     that renders as `g1xyz...`, or a username that visually collides\n//     with another). Stripped by StripBidiAndZeroWidth, which runs as\n//     the first step of every text-shaped helper.\n//   - line-ending homoglyphs: CR-only and Unicode separators\n//     (U+0085 NEL, U+2028, U+2029) that some renderers treat as line\n//     breaks. Folded uniformly.\n//   - markdown-structure injection: user content opening a heading,\n//     blockquote, list, code fence, link-reference def, setext underline,\n//     gnoweb extension delimiter, or GFM table row at document level.\n//     Strict Block escapes the line-leading `|` of any GFM table row so\n//     user content cannot inject `\u003ctable\u003e`-shaped structure; permissive\n//     BlockRich preserves table rows so authors can compose `\u003ctable\u003e`\n//     elements (gnoweb loads extension.Table per render_config.go).\n//   - HTML block type 1-5 absorption: CommonMark §4.6 HTML block types 1\n//     (`\u003cscript\u003e`, `\u003cpre\u003e`, `\u003cstyle\u003e`, `\u003ctextarea\u003e`), 2 (`\u003c!--`), 3\n//     (`\u003c?`), 4 (`\u003c!UPPER`), and 5 (`\u003c![CDATA[`) do NOT close on a blank\n//     line — they only close on a type-specific token (`\u003c/tag\u003e`, `--\u003e`,\n//     `?\u003e`, `\u003e`, `]]\u003e`) or EOF. Without a defense, user content opening\n//     any of these would swallow realm chrome appended afterward. Both\n//     Block and BlockRich line-escape the openers (prepend `\\`) so the\n//     block never opens; this defense is unconditional in both modes.\n//     Types 6 and 7 close on a blank line, so BlockRich's `\\n\\n`\n//     paragraph envelope already bounds them and no escape is needed.\n//   - realm-discipline boundary (caller's responsibility, not enforced):\n//     callers should emit realm chrome at flush-left column 0 around\n//     `BlockRich(user)`. Indented chrome (4+ leading spaces, list-item\n//     continuations, footnote-definition body, or an unclosed Type 1\n//     HTML tag in realm chrome before the call) can extend across blank\n//     lines into user content or vice versa. The sanitizer cannot\n//     defend against malformed realm chrome — only against user input.\n//   - footnote / link-reference namespace pollution: user content\n//     containing `[^name]` or `[text][label]` syntax that would otherwise\n//     resolve against realm-defined footnote definitions or link\n//     reference definitions elsewhere on the page. Block escapes the\n//     opening `[` in both shapes.\n//   - reference-link / footnote-ref / shortcut-ref collisions:\n//     `[text][label]`, `[^name]`, and bare `[label]` shortcut forms\n//     are ALL neutralized by Block's bracket walk, which preserves\n//     only inline `[text](url)` and `![alt](src)` syntax — everything\n//     else has both `[` and `]` backslash-escaped, so the parser sees\n//     literal text and can't resolve against realm-defined LRDs or\n//     footnote definitions.\n//   - multi-line LRD evasion: Block's walker recognises `[lab\\nel]: url`\n//     across newlines (single `\\n` OK, blank line aborts) and strips\n//     the whole region. `\\]` inside the label is honored as an escaped\n//     literal, so `[label\\]: url` is NOT treated as an LRD (renders as\n//     literal text).\n//   - URL scheme abuse: javascript:, data:text/html, vbscript:, blob:,\n//     protocol-relative //, mailto: with prefill phishing parameters.\n//     Allowlist-only (URL / ImageURL).\n//   - HTML attribute / element breakout: `\"`, `\u003c`, `\u003e`, `\u0026`, `'` inside\n//     HTML lexical slots. Handled by HTMLEscape.\n//   - CommonMark §2.3 NUL: replaced with U+FFFD by Block, InlineText,\n//     LinkTitle, TableCell, HTMLEscape, InlineCode, CodeBlock, and\n//     LanguageCodeBlock.\n//   - code-fence leakage: a user-opened ``` ``` ``` fence that runs to EOF\n//     with no closing fence, which would otherwise swallow every realm-\n//     emitted line that follows. Block auto-closes any open fence at EOF.\n//   - table-alignment drift: tabs inside table cells expanding to variable\n//     widths (1-4 spaces depending on column position) and shifting cell\n//     boundaries unpredictably. TableCell replaces tabs with single spaces.\n//\n// What this package does NOT do:\n//\n//   - It does not store state. Every helper is a pure function.\n//   - It does not validate semantic correctness. sanitize.URL accepts\n//     a syntactically valid https:// URL even if the host is malicious;\n//     URL reputation is a separate layer.\n//   - It does not enforce CSS containment. ImageURL admits data:image/*\n//     URIs on the assumption that the deploying gnoweb instance caps\n//     rendered image dimensions via CSS. Without that cap, a malicious\n//     image can blow out the page layout or exhaust memory.\n//   - It does not perform structural sandboxing of foreign markdown.\n//     If a realm concatenates an opaque markdown blob returned from a\n//     polymorphic interface (`someThing.Render()`), it needs a structural\n//     sandbox primitive (e.g. a `\u003cgno-card\u003e` extension), not just leaf\n//     sanitization.\n//\n// # When to use Block vs BlockRich\n//\n// Both are safe sanitizers; both run identical realm-binding defenses.\n// They differ in what user-authored block structure survives:\n//\n//   - Block — paragraph-shaped only. Escapes `#`, `\u003e`, list markers,\n//     `---`/`***`/`___` thematic breaks, and `===`/`---` setext\n//     underlines. Use for leaf slots — footnote definition bodies,\n//     table cells, blockquote bodies (Blockquote uses Block), single-\n//     paragraph prose, any slot where richer structure has no benefit\n//     or where richer structure could visually impersonate realm chrome.\n//\n//   - BlockRich — full-richness. Preserves user-authored headings,\n//     lists, quotes, HR, setext. Use for user content the realm intends\n//     to compose with full block-level structure, typically inside a\n//     sandbox container (`\u003cgno-card\u003e`, `\u003cgno-foreign\u003e`) or a CSS-demoted\n//     region. BlockRich's qualifying-setext defense prevents the\n//     cross-boundary attack (user content reaching back to promote\n//     realm chrome to a heading), but inner-heading visual containment\n//     is the realm's CSS responsibility. gnoweb does not yet ship CSS\n//     rules that demote headings inside sandbox containers — until they\n//     land, BlockRich + sandbox renders inner headings at literal size.\n//\n// Do NOT compose Block and BlockRich in either direction. Pick one\n// helper at the right level.\n//\n// # Extending\n//\n// A new helper added to this package MUST:\n//\n//  1. Be panic-free for any string input.\n//  2. Strip bidi+zero-width before any other transform (so display\n//     equals storage end-to-end).\n//  3. Declare its idempotence class in the table above.\n//  4. Document the markdown / HTML lexical slot it targets.\n//  5. Reject rather than partially-sanitize when input is structurally\n//     invalid (return \"\" — never half-process an address or URL).\n//  6. Pick exactly one of the two return-value contracts and stick to\n//     it: escapers always return a transformed string and never reject\n//     (any input is OK — the transformation makes it safe); validators\n//     return the cleaned input verbatim on accept or \"\" on reject and\n//     never half-process. Mixing the contracts within one helper is a\n//     bug — callers can't reason about whether \"\" means \"input was\n//     already empty\" or \"input was rejected\".\npackage sanitize\n\nimport (\n\t\"chain/markdown\"\n\t\"html\"\n\t\"strings\"\n)\n\n// ----- Re-exports of the public chain/markdown natives -----\n//\n// These are general-purpose data-hygiene primitives, not markdown-specific.\n// The other helpers in this package call them internally, so realms emitting\n// markdown rarely need to call them directly. Reach for these when you have\n// a non-markdown use case — e.g. normalizing a username before storage,\n// canonicalizing a search query, or stripping invisible characters from\n// any user string that will be displayed or compared.\n\n// StripBidiAndZeroWidth removes Unicode bidi controls and zero-width\n// characters (U+200B-D, U+200E-F, U+202A-E, U+2066-9, U+FEFF) from s.\n// Use it when storing or comparing user-supplied strings outside of a\n// markdown context — for example, before saving a display name to state,\n// or before hashing a search query. Idempotent: calling twice gives the\n// same result.\n//\n// Thin wrapper over chain/markdown.StripBidiAndZeroWidth.\nfunc StripBidiAndZeroWidth(s string) string {\n\treturn markdown.StripBidiAndZeroWidth(s)\n}\n\n// NormalizeBreaks unifies CR-LF and lone CR to LF (CommonMark §2.2 line\n// endings only — does NOT touch U+2028/U+2029). Use it when comparing\n// or hashing user input that may have been authored on different\n// platforms (Windows CRLF vs. Unix LF), so equivalent strings normalize\n// to the same bytes. Idempotent.\n//\n// Thin wrapper over chain/markdown.NormalizeBreaks.\nfunc NormalizeBreaks(s string) string {\n\treturn markdown.NormalizeBreaks(s)\n}\n\n// ----- Escapers -----\n\n// InlineText prepares an arbitrary user string for an INLINE markdown\n// slot — anywhere the rendered output stays on a single line and lives\n// inside a larger markdown construct.\n//\n// Use for:\n//   - link text:        [InlineText(label)](url)\n//   - heading text:     # InlineText(title)\n//   - bold/italic body: **InlineText(name)**\n//   - image alt text:   ![InlineText(alt)](src)\n//   - single-line block-context slots:\n//     \u003e [!NOTE] InlineText(title)\n//     \u003e Author: InlineText(name)\n//\n// Multi-paragraph prose belongs in Block, not InlineText. InlineText\n// folds every newline to a single space (so paragraph structure is\n// erased) and escapes inline-active CommonMark punctuation:\n//\n//\t\\ * _ [ ] ( ) ~ \u003e - + . ! ` # \u003c \u0026\n//\n// Two characters are intentionally NOT escaped:\n//\n//   - `|` — only meaningful in GFM table rows. Leaving it literal here\n//     lets TableCell (which calls InlineText then escapes `|` itself)\n//     avoid double-escaping pipes into `\\\\|`.\n//   - `=` — only meaningful as a setext heading underline, which is a\n//     line-level construct. Escaping `=` inline would mangle expressions\n//     like `x = 1` for no benefit.\n//\n// Not idempotent (see package doc).\nfunc InlineText(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = markdown.NormalizeBreaks(s)       // CM §2.2 \\r\\n / \\r → \\n\n\ts = foldNewlinesAndSeparators(s, ' ') // \\n + NEL + U+2028/U+2029 → space\n\treturn markdown.EscapeInline(s)\n}\n\n// Block prepares user content for a top-level BLOCK markdown context\n// where paragraphs, line breaks, code blocks, and other block structure\n// should survive — but where the content must NOT be able to inject\n// new top-level constructs (headings, lists, blockquotes,\n// link-reference definitions, setext underlines, gnoweb extension\n// delimiters, GFM table rows).\n//\n// Output shape: every non-empty result begins AND ends with \"\\n\\n\" —\n// CM §4.8 blank lines on both sides — so user content is guaranteed\n// to occupy its own paragraph(s), isolated from any realm chrome that\n// precedes OR follows it. This bounds CM §4.6 HTML block types 6 and\n// 7 (`\u003cdiv\u003e`, `\u003ctable\u003e`, `\u003cform\u003e`, arbitrary `\u003cfoo\u003e` tags) which\n// close on a blank line and are NOT escaped in any mode, and it\n// defeats first-line setext promotion (`===`/`---`) that strict-mode\n// escapes miss when the previous line is blank in the user input but\n// non-blank in the concatenated realm output. Empty input (or input\n// that strips entirely, e.g. a lone LRD) returns \"\" — no envelope is\n// emitted.\n//\n// Use for any multi-paragraph user-supplied prose that the realm\n// concatenates into its rendered output:\n//   - post bodies, comments, replies\n//   - profile bios, About sections\n//   - proposal descriptions, governance motions\n//   - changelog entries, release notes\n//\n// What Block does with each kind of attacker input:\n//\n//\tUser attempt                                          | Block's response\n//\t------------------------------------------------------|----------------------------------------------------\n//\t  --- preserved verbatim ---                          |\n//\t[text](url) inline link, ![alt](src) image            | preserved verbatim\n//\t------------------------------------------------------|----------------------------------------------------\n//\t  --- escaped / stripped / folded ---                 |\n//\t# heading at line-start                               | escaped → literal `# heading`\n//\t\u003e quoted at line-start                                | escaped → literal `\u003e`\n//\t- item, * item, + item, 1. item at line-start         | escaped\n//\t---, ***, ___ (3+) at line-start                      | escaped\n//\t=== or --- on its own line after non-blank text       | escaped (no setext promotion of the line above)\n//\t\u003cgno-card\u003e, \u003cgno-columns\u003e, any \u003cgno-…\u003e/\u003c/gno-…\u003e at    | escaped (wildcard match) → literal text\n//\t  line-start                                          |\n//\t| a | b | GFM table row (line-leading `|`)            | escaped → literal `| a | b |`\n//\t\u003c!--, \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e, \u003c?…?\u003e,    | escaped (\\\u003c…) → literal text;\n//\t  \u003c!DOCTYPE…\u003e, \u003c![CDATA[…]]\u003e at line-start            |   blocks goldmark from opening a\n//\t  (CM §4.6 HTML block types 1-5)                      |   blank-line-NON-terminating HTML block\n//\t[text][realm-label] ref-link USE                      | both bracket pairs escaped → \\[text\\]\\[realm-label\\]\n//\t[^name] footnote-ref                                  | both brackets escaped → \\[^name\\]\n//\t[label] bare shortcut-ref                             | both brackets escaped → \\[label\\]\n//\t[label]: url link-reference definition                | whole region stripped (incl. multi-line label\n//\t  (incl. [lab\\nel]: url multi-line)                   |   `[lab\\nel]: url` and any title continuation)\n//\t[label\\]: url (backslash-escaped `]`)                 | NOT stripped; brackets escaped → paragraph text\n//\tcode fence opened without close                       | autoclosed at end of input\n//\tNUL byte (\\x00)                                       | replaced with U+FFFD\n//\tU+2028 / U+2029 / U+0085 (NEL)                        | folded to `\\n`\n//\tbidi/zero-width controls                              | stripped\n//\n// COMPOSITION GOTCHA: Block's EOF fence-autoclose appends a final\n// fence line. If you wrap Block's output with a line-prefixing\n// builder like md.Blockquote (which prepends `\u003e ` per line) or\n// md.Nested, that closing fence becomes a prefixed line. The output\n// is still safe (the fence still closes correctly) but may render\n// awkwardly. If pixel-perfect output matters, strip a trailing blank\n// fence line after Block.\n//\n// Why backslash and not a space for `\u003cgno-…\u003e` lines: gnoweb's\n// extension parsers call `util.TrimLeftSpace` on the line before tag\n// matching, which would strip a leading space and let the tag match\n// anyway. A leading `\\` survives the trim (only ASCII whitespace +\n// form-feed are stripped) and is consumed by the inline escape phase\n// before Type-7 HTML block detection can fire (Type-7 requires the\n// first non-whitespace char to be `\u003c`).\n//\n// Inline emphasis, code spans, inline links, and soft line breaks\n// within a paragraph are PRESERVED — users can format. Pipes that\n// are NOT at line-start stay literal so prose can still write things\n// like `a | b`.\n//\n// Idempotent: Block(Block(s)) is byte-identical to Block(s). The\n// bracket walker strips LRDs on the first pass; remaining `[`/`]`\n// outside inline-link/image spans are escaped to `\\[`/`\\]`, and\n// already-escaped brackets are preserved on subsequent passes\n// (pass-2 backslash-parity tracking). Still, wrap each user-supplied\n// string exactly once — chained sanitization adds no value and\n// burns gas.\nfunc Block(s string) string {\n\ts = markdown.NormalizeBreaks(s)\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = replaceNULWithFFFD(s)\n\ts = markdown.EscapeBlockHazards(s)\n\t// Symmetric \"\\n\\n\" envelope — same pattern BlockRich uses for the\n\t// same reasons (see BlockRich docstring \"Cross-paragraph safety\").\n\t// Strict mode escapes most line-leading hazards (setext, GFM table\n\t// row, CM §4.6 HTML types 1-5, list/heading/HR markers), but two\n\t// hazards remain that only a blank-line break can close:\n\t//\n\t//   - CM §4.6 HTML block types 6 and 7 (`\u003cdiv\u003e`, `\u003ctable\u003e`,\n\t//     `\u003cform\u003e`, arbitrary `\u003cfoo\u003e` tags) are NOT escaped in any mode\n\t//     — they close on a blank line per CM. Without a trailing\n\t//     \"\\n\\n\", a `\u003cdiv\u003e` at the end of user content extends into\n\t//     appended realm chrome.\n\t//\n\t//   - First-line setext: strict mode's setext escape only fires\n\t//     when the previous line is non-blank IN THE USER'S INPUT.\n\t//     A user whose first line is `===` slips past, and concatenated\n\t//     after `chrome\\n` would promote chrome to H1. The leading\n\t//     \"\\n\\n\" forces a paragraph break so chrome cannot be merged.\n\t//\n\t// TrimLeft/TrimRight + fixed wrap is idempotent: Block(Block(s)) is\n\t// byte-identical to Block(s). Empty post-escape result short-\n\t// circuits to \"\" so realm concatenation doesn't leak stray blank\n\t// lines for trivially empty inputs (e.g. lone LRD that strips\n\t// entirely).\n\ts = strings.TrimLeft(s, \"\\n\")\n\ts = strings.TrimRight(s, \"\\n\")\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"\\n\\n\" + s + \"\\n\\n\"\n}\n\n// BlockRich is the permissive counterpart of Block. Both are safe\n// sanitizers — the distinction is what markdown structure survives:\n//\n//   - Block escapes line-leading block markers (`#`, `\u003e`, `-`, `*`,\n//     `+`, `1.`), thematic breaks (`---`/`***`/`___`), and setext\n//     underlines (`===`/`---`). User content becomes paragraph-shaped.\n//   - BlockRich PRESERVES all of those, so user content can compose\n//     headings, lists, quotes, horizontal rules, and setext-styled\n//     headings. Realm-binding defenses stay on (extension delimiters\n//     `\u003cgno-…\u003e`, the bracket walker for link / LRD / ref /\n//     footnote / shortcut, fence autoclose, NUL / bidi /\n//     Unicode-separator folding). GFM table-row openers are\n//     PRESERVED (see \"Tables\" below).\n//\n// Cross-paragraph safety: BlockRich's output begins with \"\\n\\n\"\n// AND ends with \"\\n\\n\" — CM §4.8 blank lines on both sides — so\n// user content is guaranteed to occupy its own paragraph(s),\n// isolated from anything the realm emits before OR after. Symmetric\n// isolation closes four distinct attacks:\n//\n//   - Cross-paragraph setext promotion (backward). User content\n//     `body\\n===\\nmore` concatenated after realm chrome (no\n//     trailing `\\n`) would, without paragraph isolation, place\n//     \"chrome\\nbody\" in one paragraph; the `===` setext underline\n//     would then promote that merged paragraph to H1, hijacking\n//     realm chrome. The leading \"\\n\\n\" forces a paragraph break.\n//\n//   - Cross-paragraph GFM table promotion (backward). User content\n//     beginning with `|---|---|` (a table delimiter row) would,\n//     without a blank-line break, retroactively turn the preceding\n//     realm line into a `\u003cthead\u003e`. Paragraph isolation prevents\n//     the table-detection scan from crossing the boundary.\n//\n//   - Cross-paragraph GFM table promotion (forward). Realm chrome\n//     appended immediately after BlockRich(user) that begins with\n//     `|---|` would, without a trailing blank line, extend user's\n//     last line into a table header and pull realm chrome into the\n//     body row. The trailing \"\\n\\n\" prevents the merge.\n//\n//   - Lazy paragraph continuation (forward). Paragraph-shaped realm\n//     chrome appended immediately after BlockRich(user) would, via\n//     CM §5.2, merge into user's trailing paragraph and inherit any\n//     block-level decoration it carries.\n//\n// First-line qualifying-setext escape (the\n// `neuterLeadingSetextIfQualifying` pre-pass) remains in place as\n// belt-and-suspenders: if the first non-blank line of user input\n// matches the CM §4.3 setext-underline pattern (run of `=` or `-`\n// with 0-3 leading spaces and only trailing whitespace), BlockRich\n// inserts `\\` before the first `=`/`-`. This is redundant given\n// paragraph isolation but harmless and inexpensive.\n//\n// # Tables\n//\n// BlockRich preserves line-leading `|` so user content can\n// compose GFM tables:\n//\n//\t| Header A | Header B |\n//\t|----------|----------|\n//\t| cell a   | cell b   |\n//\n// renders as a real `\u003ctable\u003e` element. Strict Block continues to\n// escape line-leading `|` (each row becomes literal `\\| a | b |`\n// text). When the realm authors the table itself and inserts user\n// content into a specific cell, use TableCell — NOT BlockRich —\n// to sanitize that cell value.\n//\n// What attacker input produces what (full table, same rows as Block\n// except where marked CHANGED):\n//\n//\tUser attempt                                  | BlockRich response\n//\t----------------------------------------------|--------------------------------------------------\n//\t  --- preserved (compose freely) ---          |\n//\t# heading at line-start                       | preserved [CHANGED from Block]\n//\t\u003e quoted at line-start                        | preserved [CHANGED]\n//\t- item, * item, + item, 1. item               | preserved [CHANGED]\n//\t---, ***, ___ thematic break                  | preserved [CHANGED]\n//\t=== or --- setext underline                   | preserved when preceded by user text;\n//\t                                              | escaped (\\===/\\---) if the first non-blank\n//\t                                              | line of input [CHANGED]\n//\t| a | b | GFM table row (line-leading |)      | preserved → renders as \u003ctable\u003e when followed by\n//\t                                              | a delimiter row [CHANGED]\n//\t[text](url), ![alt](src)                      | preserved verbatim [SAME]\n//\t----------------------------------------------|--------------------------------------------------\n//\t  --- escaped / stripped / folded ---         |\n//\t\u003cgno-card\u003e, any \u003cgno-…\u003e/\u003c/gno-…\u003e at line-start| escaped (wildcard match) [SAME]\n//\t\u003c!--, \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e,   | escaped (\\\u003c…) [SAME] — Types 1-5 don't close\n//\t  \u003c?…?\u003e, \u003c!DOCTYPE…\u003e, \u003c![CDATA[…]]\u003e           |   on blank lines, so `\\n\\n` envelope\n//\t  at line-start (CM §4.6 HTML block types 1-5)|   doesn't isolate them; explicit escape\n//\t[text][realm-label] ref-link USE              | both pairs escaped [SAME]\n//\t[^name] footnote-ref                          | both brackets escaped [SAME]\n//\t[label] bare shortcut-ref                     | both brackets escaped [SAME]\n//\t[label]: url link-reference definition        | whole region stripped [SAME]\n//\t[label\\]: url (escaped `]`)                   | not stripped; brackets escaped [SAME]\n//\tcode fence opened without close               | autoclosed at end of input [SAME]\n//\tNUL byte (\\x00)                               | replaced with U+FFFD [SAME]\n//\tU+2028 / U+2029 / U+0085 (NEL)                | folded to `\\n` [SAME]\n//\tbidi/zero-width controls                      | stripped [SAME]\n//\n// Use BlockRich for user content the realm intends to compose with\n// full block-level richness — typically inside a sandbox container\n// (`\u003cgno-card\u003e`, `\u003cgno-foreign\u003e`) or a CSS-demoted region where inner\n// headings render visually distinct from realm chrome. The realm\n// must own the visual containment: concatenating BlockRich's output\n// directly into a top-level page still lets the user write `# heading`\n// at document level. BlockRich's cross-boundary setext defense prevents\n// the worst case (reaching backwards into realm bytes), but visual\n// containment of inner headings is the realm's CSS responsibility.\n// gnoweb does not yet ship CSS rules that demote inner headings inside\n// `\u003cgno-card\u003e` / `\u003cgno-foreign\u003e` — until those rules land, realms\n// using BlockRich + a sandbox should be aware that inner headings\n// render at their literal level.\n//\n// Idempotent: BlockRich(BlockRich(s)) is byte-identical to\n// BlockRich(s). The TrimLeft-then-\"\\n\\n\"-prepend pattern strips\n// any leading newlines and reapplies exactly two, so the leading\n// shape is stable across passes; the qualifying-setext escape is\n// stable (a line beginning with `\\` no longer matches the setext\n// pattern); and the bracket walker treats already-escaped\n// `\\[`/`\\]` as ordinary bytes. Empty input (or input that strips\n// to empty, e.g. a lone link-reference definition) returns \"\" —\n// realm concatenation doesn't get a stray blank line.\n// Still, wrap each user-supplied string exactly once — chained\n// sanitization adds no value and burns gas.\n//\n// Realm-discipline boundary: BlockRich defends user input against\n// every cross-paragraph attack listed above, but it CANNOT defend\n// against malformed REALM chrome. Specifically, callers should emit\n// realm chrome at flush-left column 0 around `BlockRich(user)`. If\n// the realm chrome BEFORE the call contains an unclosed CM §4.6\n// Type 1 HTML tag (`\u003cscript\u003e`, `\u003cpre\u003e`, `\u003cstyle\u003e`, `\u003ctextarea\u003e`),\n// the `\\n\\n` envelope does NOT close it (Type 1 closes only on the\n// matching close tag), and user-controlled `\u003c/tag\u003e` content can\n// then prematurely terminate it. Indented chrome (4+ leading\n// spaces, list-item continuations, footnote-definition body) can\n// likewise extend across the envelope into user content. Keep\n// chrome flush-left and Type 1 tags closed within the chrome.\n//\n// PREVIEW: BlockquoteRich is currently the only in-tree caller of\n// BlockRich; the API and the `\"\\n\\n\"` output shape may evolve once\n// direct callers emerge.\nfunc BlockRich(s string) string {\n\ts = markdown.NormalizeBreaks(s)\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = replaceNULWithFFFD(s)\n\t// Fold Unicode separators (U+2028, U+2029, U+0085 NEL) to '\\n'\n\t// BEFORE the setext-qualifying check. The native\n\t// EscapeBlockHazardsRich also folds them internally, but the Gno\n\t// helper below needs to see the folded form to correctly identify\n\t// the first non-blank line — otherwise an attacker can hide the\n\t// `===` setext underline behind a U+2028 / U+2029 / U+0085 and\n\t// reach back to promote realm chrome above it.\n\ts = foldSeparatorsToNewline(s)\n\ts = neuterLeadingSetextIfQualifying(s)\n\ts = markdown.EscapeBlockHazardsRich(s)\n\t// Ensure the output BOTH starts AND ends with \"\\n\\n\" — CM §4.8\n\t// blank lines on each side — so user content is GUARANTEED to\n\t// occupy its own paragraph(s), isolated from anything the realm\n\t// emits before OR after. Symmetric isolation closes four attacks:\n\t//\n\t//  Backward (closed by leading \"\\n\\n\"):\n\t//   1. Deeper-setext: user content `body\\n===\\nmore` concatenated\n\t//      after realm chrome (no trailing `\\n`) would otherwise place\n\t//      \"chrome\\nbody\" in one paragraph; the `===` setext underline\n\t//      would then promote that merged paragraph to H1, hijacking\n\t//      realm chrome.\n\t//   2. GFM table-row promotion: user content beginning with\n\t//      `|---|---|` (a table delimiter row) would, without a blank-\n\t//      line break, retroactively promote the preceding realm line\n\t//      into a `\u003cthead\u003e` cell.\n\t//\n\t//  Forward (closed by trailing \"\\n\\n\"):\n\t//   3. GFM table-row promotion in reverse: realm appending its own\n\t//      chrome immediately after BlockRich(user), where chrome\n\t//      starts with `|---|`, would extend user's last line into a\n\t//      table header and pull realm chrome into the body row.\n\t//   4. Lazy paragraph continuation: realm appending paragraph-\n\t//      shaped chrome immediately after BlockRich(user) would, via\n\t//      CM §5.2 lazy-continuation, merge into user's trailing\n\t//      paragraph and inherit any block-level decoration it carries.\n\t//\n\t// `neuterLeadingSetextIfQualifying` above is now belt-and-\n\t// suspenders for the first-line setext case: even if the blank-\n\t// line guarantee were somehow defeated by an exotic CM consumer,\n\t// the first-line escape still blocks the simplest setext shape.\n\t//\n\t// Empty post-escape result short-circuits to \"\" so realm\n\t// concatenation doesn't leak stray blank lines for trivially empty\n\t// inputs (e.g. a lone link-reference definition that strips\n\t// entirely).\n\t//\n\t// Idempotency: TrimLeft and TrimRight strip ALL leading/trailing\n\t// \"\\n\"s, then the wrap adds exactly two on each side. Stable\n\t// across passes.\n\ts = strings.TrimLeft(s, \"\\n\")\n\ts = strings.TrimRight(s, \"\\n\")\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\treturn \"\\n\\n\" + s + \"\\n\\n\"\n}\n\n// foldSeparatorsToNewline replaces U+0085 NEL (0xC2 0x85),\n// U+2028 (0xE2 0x80 0xA8), and U+2029 (0xE2 0x80 0xA9) with '\\n'.\n// Leaves '\\n' bytes alone. Used by BlockRich so the qualifying-setext\n// pre-pass and the native both see the same line structure.\nfunc foldSeparatorsToNewline(s string) string {\n\t// Cheap pre-check: only the 0xC2 / 0xE2 lead bytes can trigger.\n\tif !containsAnyByteForFold(s) {\n\t\treturn s\n\t}\n\tout := make([]byte, 0, len(s))\n\tfor i := 0; i \u003c len(s); {\n\t\tc := s[i]\n\t\tif c == 0xC2 \u0026\u0026 i+1 \u003c len(s) \u0026\u0026 s[i+1] == 0x85 {\n\t\t\tout = append(out, '\\n')\n\t\t\ti += 2\n\t\t\tcontinue\n\t\t}\n\t\tif c == 0xE2 \u0026\u0026 i+2 \u003c len(s) \u0026\u0026 s[i+1] == 0x80 \u0026\u0026 (s[i+2] == 0xA8 || s[i+2] == 0xA9) {\n\t\t\tout = append(out, '\\n')\n\t\t\ti += 3\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, c)\n\t\ti++\n\t}\n\treturn string(out)\n}\n\nfunc containsAnyByteForFold(s string) bool {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tif s[i] == 0xC2 || s[i] == 0xE2 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// neuterLeadingSetextIfQualifying scans s for the first non-blank\n// line. If that line matches the CommonMark §4.3 setext-underline\n// pattern (0-3 leading spaces, then a run of all `=` or all `-`,\n// then optional trailing whitespace, then `\\n` or EOF), the function\n// returns s with a `\\` inserted before the first `=`/`-`. Otherwise\n// returns s unchanged. The escape prevents a realm-emitted line above\n// BlockRich's output from being retroactively promoted to a heading.\nfunc neuterLeadingSetextIfQualifying(s string) string {\n\tpos := 0\n\tfor pos \u003c len(s) {\n\t\t// Walk to the first non-whitespace byte of the current line.\n\t\tlineStart := pos\n\t\ti := pos\n\t\tfor i \u003c len(s) \u0026\u0026 (s[i] == ' ' || s[i] == '\\t') {\n\t\t\ti++\n\t\t}\n\t\tif i \u003e= len(s) || s[i] == '\\n' {\n\t\t\t// Blank line; advance to next line.\n\t\t\tif i \u003e= len(s) {\n\t\t\t\treturn s\n\t\t\t}\n\t\t\tpos = i + 1\n\t\t\tcontinue\n\t\t}\n\t\t// First non-blank line. Check setext-underline shape.\n\t\tif i-lineStart \u003e 3 {\n\t\t\treturn s // 4+ leading spaces = indented code, not setext\n\t\t}\n\t\tc := s[i]\n\t\tif c != '=' \u0026\u0026 c != '-' {\n\t\t\treturn s // not a setext underline candidate\n\t\t}\n\t\tj := i + 1\n\t\tfor j \u003c len(s) \u0026\u0026 s[j] == c {\n\t\t\tj++\n\t\t}\n\t\tfor j \u003c len(s) \u0026\u0026 (s[j] == ' ' || s[j] == '\\t') {\n\t\t\tj++\n\t\t}\n\t\tif j \u003c len(s) \u0026\u0026 s[j] != '\\n' {\n\t\t\treturn s // mixed content on the line — not setext\n\t\t}\n\t\treturn s[:i] + \"\\\\\" + s[i:]\n\t}\n\treturn s\n}\n\n// Blockquote wraps user content as a CommonMark blockquote: each line\n// of the cleaned content gets a \"\u003e \" prefix so the renderer displays\n// it inside a `\u003cblockquote\u003e` element.\n//\n// Use for any multi-paragraph user-supplied text that the realm wants\n// to render as a quotation: cited posts, attached responses, error\n// snapshots that should visually stand out.\n//\n// The content is first cleaned by Block (bidi-strip, line-ending\n// normalize, NUL→U+FFFD, bracket walker for link/image/LRD spans,\n// block-marker escape, code-fence auto-close at EOF, Unicode-separator\n// fold). Block's \"\\n\\n\" cross-paragraph envelope is then stripped —\n// the `\u003e ` marker creates the container boundary, so the envelope\n// would only line-prefix to empty `\u003e ` lines top and bottom — and\n// every remaining line is prefixed with \"\u003e \". The user content can\n// still use inline emphasis, code spans, and nested fenced code blocks\n// inside the quote; what it cannot do is open new top-level structure\n// (heading, list, blockquote, GFM table row, etc.) or escape the\n// quote.\n//\n// Output shape — every non-empty result begins with \"\\n\" and ends\n// with \"\\n\\n\" (same shape as BlockquoteRich):\n//\n//   - Leading \"\\n\" guarantees a clean blockquote opener even when the\n//     realm concatenates `chrome + Blockquote(user)` without its own\n//     newline separator.\n//   - Trailing \"\\n\\n\" (blank line) cleanly ends the blockquote so a\n//     realm appending `Blockquote(user) + chrome` cannot pull chrome\n//     bytes into the quote via CommonMark §5.2 lazy continuation.\n//\n// Empty input (or input that strips entirely, e.g. a lone LRD)\n// returns \"\" — no blockquote is emitted.\n//\n// Composition gotcha: Block's EOF code-fence auto-close (added when\n// user content opens a ``` ``` ``` fence without closing it) becomes a\n// \"\u003e ```\" line at the end of the blockquote. Goldmark parses this\n// correctly as the close of a fenced block inside the quote — the\n// output is structurally safe — but the markdown source looks unusual\n// to a human reviewer. If aesthetic output matters, ensure user\n// content closes its own fences.\n//\n// Not idempotent (see package doc): wraps with `\u003e ` per line, so\n// calling twice double-wraps and the outer call's Block step escapes\n// the inner `\u003e` prefixes.\n//\n// Do NOT compose with BlockRich in either direction:\n//   - Blockquote(BlockRich(s)) double-sanitizes: BlockRich preserves\n//     `#`/`\u003e`/etc., then Blockquote's Block step escapes them again.\n//   - BlockRich(Blockquote(s)) doesn't make sense: Blockquote already\n//     line-prefixed with `\u003e `; BlockRich expects raw user content.\n//\n// For a quoted body that can contain headings, lists, nested quotes,\n// or thematic breaks, use BlockquoteRich.\nfunc Blockquote(text string) string {\n\ttext = Block(text)\n\t// Block wraps its output with \"\\n\\n\" on each side for cross-\n\t// paragraph isolation. Inside a blockquote both wraps are redundant\n\t// — the `\u003e ` marker creates the container boundary — and they\n\t// would line-prefix to two useless `\u003e ` empty quoted lines top and\n\t// bottom. Strip ALL leading and trailing \"\\n\"s so the body starts\n\t// and ends clean; this helper re-wraps with `\\n` + body + `\\n\\n`\n\t// below (same shape as BlockquoteRich).\n\ttext = strings.TrimLeft(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\ttext = strings.TrimRight(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\tvar sb strings.Builder\n\tsb.WriteByte('\\n')\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tsb.WriteString(\"\u003e \")\n\t\tsb.WriteString(line)\n\t\tsb.WriteByte('\\n')\n\t}\n\tsb.WriteByte('\\n')\n\treturn sb.String()\n}\n\n// BlockquoteRich is the permissive counterpart of Blockquote. Both\n// wrap user content as a CommonMark blockquote (each line prefixed\n// with `\u003e `), but they differ in what block-level structure inside\n// the quote survives:\n//\n//   - Blockquote escapes line-leading block markers, so the quoted\n//     body is paragraph-shaped — `# x` inside a Blockquote stays a\n//     literal `#`.\n//   - BlockquoteRich PRESERVES line-leading block markers, so the\n//     quoted body can compose ATX headings, lists, thematic breaks,\n//     nested blockquotes (`\u003e \u003e nested`), and other block-level\n//     structure. Realm-binding defenses stay on (extension delimiters,\n//     GFM table-row openers, bracket walker, fence autoclose,\n//     NUL / bidi / Unicode-separator folding).\n//\n// Output shape — every non-empty result begins with \"\\n\" and ends\n// with \"\\n\\n\":\n//\n//   - Leading \"\\n\" guarantees a clean blockquote opener even when the\n//     realm concatenates `chrome + BlockquoteRich(user)` without its\n//     own newline separator. Without the leading \"\\n\", chrome ending\n//     mid-line followed by \"\u003e quoted\" would render `\u003e` as literal\n//     paragraph text instead of opening a blockquote.\n//   - Trailing \"\\n\\n\" (blank line) cleanly ends the blockquote so a\n//     realm appending `BlockquoteRich(user) + chrome` cannot pull\n//     chrome bytes into the quote via CommonMark §5.2 lazy\n//     continuation. Without the trailing blank line, paragraph chrome\n//     immediately after BlockquoteRich would render inside the quote.\n//   - BlockRich's own leading \"\\n\\n\" (paragraph-isolation blank line)\n//     is stripped before line-prefixing — otherwise the output would\n//     carry one or two redundant empty `\u003e ` quoted lines at the top.\n//     A single \"\\n\" is then re-prepended at the BlockquoteRich\n//     boundary so `chrome + BlockquoteRich(user)` still lands the\n//     first `\u003e` at column 0.\n//   - The cross-boundary setext defense BlockRich provides is\n//     redundant inside a blockquote: a setext underline inside `\u003e `\n//     content can only promote a line in the same blockquote, never\n//     reach realm bytes (different CM container). BlockRich still\n//     applies it, harmlessly.\n//\n// What attacker input produces what (rows that differ from\n// Blockquote are marked CHANGED):\n//\n//\tUser attempt                                  | BlockquoteRich response\n//\t----------------------------------------------|------------------------------------------------\n//\t  --- preserved inside `\u003e ` quote ---         |\n//\t# heading                                     | preserved as `\u003e # heading` [CHANGED]\n//\t\u003e nested quote                                | preserved as `\u003e \u003e nested quote` [CHANGED]\n//\t- item, * item, + item, 1. item               | preserved as `\u003e - item` etc. [CHANGED]\n//\t---, ***, ___ thematic break                  | preserved [CHANGED]\n//\t=== or --- setext underline                   | preserved when preceded by user text;\n//\t                                              | escaped (\\===/\\---) if first non-blank\n//\t                                              | line of input [CHANGED]\n//\t| a | b | GFM table row (line-leading |)      | preserved → renders as \u003ctable\u003e inside the\n//\t                                              | blockquote when followed by a delimiter row [CHANGED]\n//\t[text](url), ![alt](src)                      | preserved verbatim [SAME]\n//\t----------------------------------------------|------------------------------------------------\n//\t  --- escaped / stripped / folded ---         |\n//\t\u003cgno-card\u003e, any \u003cgno-…\u003e/\u003c/gno-…\u003e at line-start| escaped (wildcard match) [SAME]\n//\t\u003c!--, \u003cscript\u003e, \u003cpre\u003e, \u003cstyle\u003e, \u003ctextarea\u003e,   | escaped (\\\u003c…) [SAME] — CM §4.6 Types 1-5\n//\t  \u003c?…?\u003e, \u003c!DOCTYPE…\u003e, \u003c![CDATA[…]]\u003e           |   don't close on blank lines; without escape\n//\t  at line-start                               |   they would swallow chrome past the `\u003e ` quote\n//\t[text][realm-label] ref-link USE              | both pairs escaped [SAME]\n//\t[^name] footnote-ref                          | both brackets escaped [SAME]\n//\t[label] bare shortcut-ref                     | both brackets escaped [SAME]\n//\t[label]: url link-reference definition        | whole region stripped [SAME]\n//\tcode fence opened without close               | autoclosed at end of input [SAME]\n//\tNUL byte (\\x00)                               | replaced with U+FFFD [SAME]\n//\tU+2028 / U+2029 / U+0085 (NEL)                | folded to `\\n` [SAME]\n//\tbidi/zero-width controls                      | stripped [SAME]\n//\n// Use BlockquoteRich when the realm wants to render user content as\n// a quotation that itself reads like authored markdown — the visual\n// CSS containment of `\u003cblockquote\u003e` already demotes inner headings\n// relative to realm chrome, so the \"inner headings need a sandbox\"\n// caveat that applies to BlockRich at top level does not apply here.\n//\n// Not idempotent: like Blockquote, calling twice double-wraps —\n// `BlockquoteRich(BlockquoteRich(s))` produces `\u003e \u003e content`,\n// nesting the quote a level deeper each pass.\n//\n// Empty input (or input that reduces to nothing after BlockRich,\n// e.g. a lone link-reference definition) returns \"\" — no blockquote\n// is emitted and neither the leading \"\\n\" nor the trailing \"\\n\\n\"\n// shape applies.\nfunc BlockquoteRich(text string) string {\n\ttext = BlockRich(text)\n\t// BlockRich wraps user content with \"\\n\\n\" on each side for\n\t// cross-paragraph isolation. Inside a blockquote both wraps are\n\t// redundant — the `\u003e ` marker creates the container boundary —\n\t// and they would line-prefix to two useless `\u003e ` empty quoted\n\t// lines top and bottom. Strip ALL leading and trailing \"\\n\"s so\n\t// the body starts and ends clean; this helper re-wraps with `\\n`\n\t// + body + `\\n\\n` below.\n\ttext = strings.TrimLeft(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\t// Strip ALL trailing newlines so the loop produces exactly one\n\t// `\u003e line` per content line, then append `\\n\\n` at the end so the\n\t// blockquote terminates cleanly (see \"Output shape\" above).\n\ttext = strings.TrimRight(text, \"\\n\")\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\tvar sb strings.Builder\n\t// Leading \"\\n\" so `chrome + BlockquoteRich(user)` cannot land the\n\t// first `\u003e` mid-line.\n\tsb.WriteByte('\\n')\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tsb.WriteString(\"\u003e \")\n\t\tsb.WriteString(line)\n\t\tsb.WriteByte('\\n')\n\t}\n\t// Trailing blank line so `BlockquoteRich(user) + chrome` cannot\n\t// pull chrome into the quote via lazy continuation.\n\tsb.WriteByte('\\n')\n\treturn sb.String()\n}\n\n// LinkTitle prepares user content for a CommonMark link-title or\n// image-title slot — the optional quoted text after the URL in any of\n// these forms:\n//\n//\t[text](url \"TITLE\")\n//\t![alt](src \"TITLE\")\n//\t[label]: url \"TITLE\"\n//\n// Escapes the inline-active set plus `\"` and `'` (the title delimiters\n// that aren't already in the inline set; `(` and `)` are), so the\n// caller can choose any of the three title-quote styles safely.\n//\n// Pick the right helper for the slot — markdown title and HTML\n// attribute share the look but use different escape rules:\n//\n//\t[text](url \"X\")              → LinkTitle      (markdown title)\n//\t\u003ca title=\"X\"\u003e                → HTMLEscape     (HTML attribute)\n//\t\u003ch5\u003eX\u003c/h5\u003e                   → HTMLEscape     (HTML element body)\n//\n// Swapping HTMLEscape for LinkTitle is wrong: HTML's `\u0026amp;` written\n// inside a markdown title renders as the literal characters `\u0026amp;`.\n// Swapping LinkTitle for HTMLEscape is wrong: markdown's `\\\"` survives\n// into the rendered HTML as a literal backslash-quote.\n//\n// Not idempotent (see package doc).\nfunc LinkTitle(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = markdown.NormalizeBreaks(s)\n\ts = foldNewlinesAndSeparators(s, ' ')\n\treturn markdown.EscapeTitle(s)\n}\n\n// TableCell prepares user content for a GFM table cell — the bytes\n// between two `|` column delimiters in a table row like\n// `| cell-a | cell-b | cell-c |`. An unescaped `|` inside cell\n// content would open a new column, letting a malicious user shift\n// every column to its right.\n//\n// On top of InlineText's behavior, TableCell:\n//   - escapes `|` to `\\|` so user content can't end the cell early.\n//   - replaces tabs with single spaces. CommonMark expands tabs to\n//     the next multiple-of-4 column boundary (variable 1-4 spaces),\n//     which would shift the displayed cell-content width unpredictably\n//     and confuse table alignment.\n//\n// Not idempotent (see package doc).\nfunc TableCell(s string) string {\n\ts = InlineText(s)\n\ts = strings.ReplaceAll(s, \"\\t\", \" \")\n\ts = strings.ReplaceAll(s, \"|\", `\\|`)\n\treturn s\n}\n\n// HTMLEscape prepares user content for an HTML lexical slot inside\n// markdown — covers attribute values, element bodies, and HTML\n// comment bodies:\n//\n//\t\u003cgno-card type=\"...\" caption=\"X\"\u003e         attribute value\n//\t\u003cgno-alert title=\"X\"\u003e                     attribute value\n//\t\u003ch5\u003eX\u003c/h5\u003e                                element body\n//\t\u003cdetails\u003e\u003csummary\u003eX\u003c/summary\u003e...          element body\n//\t\u003c!-- X --\u003e                                comment body (safe: `\u003e`\n//\t                                          becomes `\u0026gt;`, so user\n//\t                                          cannot inject `--\u003e`)\n//\n// HTMLEscape escapes the union of attribute-breaking and body-breaking\n// characters (`\u003c`, `\u003e`, `\u0026`, `\"`, `'`), so one function safely serves\n// every HTML lexical context. Callers don't have to remember which\n// subset to use for which slot.\n//\n// Pick the right helper — markdown title and HTML attribute share\n// the look but use different escape rules:\n//\n//\t[text](url \"X\")              → LinkTitle      (markdown title)\n//\t\u003cspan title=\"X\"\u003e             → HTMLEscape     (HTML attribute)\n//\t\u003ch5\u003eX\u003c/h5\u003e                   → HTMLEscape     (HTML element body)\n//\n// Swapping InlineText for HTMLEscape is wrong: markdown's backslash\n// escapes survive into the rendered HTML as literal `\\*`. Swapping\n// LinkTitle for HTMLEscape is also wrong: `\u0026amp;` written inside a\n// markdown title renders as the literal characters `\u0026amp;`.\n//\n// Not idempotent (see package doc): calling twice produces\n// `\u0026amp;` → `\u0026amp;amp;`.\nfunc HTMLEscape(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\ts = markdown.NormalizeBreaks(s)\n\ts = foldNewlinesAndSeparators(s, ' ')\n\ts = replaceNULWithFFFD(s)\n\treturn html.EscapeString(s)\n}\n\n// ----- URL filters -----\n\n// URL validates a URL for use as a link href, percent-encodes unsafe\n// bytes, and rejects anything outside the allowlist of schemes.\n//\n// Allowlist:\n//   - http, https\n//   - mailto (rejected if it carries any query: prefill phishing via\n//     body, subject, cc, etc. Both '?' and '\u0026' are rejected; see\n//     linkSchemeAllowed for why '\u0026' counts.)\n//   - any URL WITHOUT a scheme — relative paths (`/path`, `./rel`,\n//     `bare-path`), query-only (`?q=v`), fragment-only (`#anchor`).\n//     A `:` appearing inside the URL (e.g. `/path:foo`, `?q=a:b`) is\n//     NOT a scheme separator per RFC 3986 — only `:` immediately after\n//     a leading `[a-zA-Z][a-zA-Z0-9+.-]*` counts.\n//\n// Rejected (have an unknown scheme):\n//   - javascript:, data:, vbscript:, blob:, file:, etc.\n//   - `//host/...` (protocol-relative — tracking-pixel vector)\n//\n// Returns \"\" if the URL is empty after trim or fails the allowlist.\nfunc URL(s string) string {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tif !linkSchemeAllowed(s) {\n\t\treturn \"\"\n\t}\n\treturn markdown.PercentEncodeURL(s)\n}\n\n// ImageURL validates a URL for use as an image src. Kept separate from\n// URL — not a parameterized variant — because the allowlist shapes\n// differ qualitatively (data:image/* vs. mailto:) and a single boolean\n// flag would invite callers to pass the wrong default.\n//\n// Allowlist:\n//   - http, https\n//   - schemeless relative URLs starting with /, ./, or ..\n//     (rejects // protocol-relative — tracking-pixel vector)\n//   - data:image/svg+xml, data:image/png, data:image/jpeg,\n//     data:image/gif, data:image/webp\n//\n// Any other data: subtype is rejected — data:text/html etc. would\n// render as inline HTML and execute embedded scripts.\n//\n// DEPLOYMENT PRECONDITION: data: URIs encode the bytes of the image\n// directly into the markup, so a malicious sender can construct an\n// image whose pixel dimensions are arbitrarily large at minimal byte\n// cost. The deploying gnoweb instance MUST clamp rendered image\n// dimensions via CSS (e.g. `max-width: 100%; max-height: \u003cbound\u003e`).\n// Without that cap, a single image can blow out the page layout or\n// exhaust the browser's memory.\n//\n// Returns \"\" if the URL is empty after trim or fails the allowlist.\nfunc ImageURL(s string) string {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tif !imageSchemeAllowed(s) {\n\t\treturn \"\"\n\t}\n\treturn markdown.PercentEncodeURL(s)\n}\n\n// ----- Validators -----\n\n// userNameCharsets builds the [2]uint64 bitmaps for the r/sys/users\n// charset: first [a-z], rest [a-z0-9_-]. Initialized once at package\n// init.\nvar (\n\tuserNameFirstLo, userNameFirstHi           uint64\n\tuserNameRestLo, userNameRestHi             uint64\n\tfootnoteLabelFirstLo, footnoteLabelFirstHi uint64\n\tfootnoteLabelRestLo, footnoteLabelRestHi   uint64\n\tlangFirstLo, langFirstHi                   uint64\n\tlangRestLo, langRestHi                     uint64\n\tbechHrpFirstLo, bechHrpFirstHi             uint64\n\tbechHrpRestLo, bechHrpRestHi               uint64\n\tbechDataFirstLo, bechDataFirstHi           uint64\n\tbechDataRestLo, bechDataRestHi             uint64\n)\n\nfunc init() {\n\t// UserName: first [a-z], rest [a-z0-9_-].\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026userNameFirstLo, \u0026userNameFirstHi, c)\n\t\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, c)\n\t}\n\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, '_')\n\tsetBit(\u0026userNameRestLo, \u0026userNameRestHi, '-')\n\n\t// FootnoteLabel: [A-Za-z0-9_-] for both first and rest.\n\tfor c := byte('A'); c \u003c= 'Z'; c++ {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\tfor _, c := range []byte{'_', '-'} {\n\t\tsetBit(\u0026footnoteLabelFirstLo, \u0026footnoteLabelFirstHi, c)\n\t\tsetBit(\u0026footnoteLabelRestLo, \u0026footnoteLabelRestHi, c)\n\t}\n\n\t// LanguageName: [a-zA-Z0-9_+-] for both first and rest.\n\tfor c := byte('A'); c \u003c= 'Z'; c++ {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\tfor _, c := range []byte{'_', '+', '-'} {\n\t\tsetBit(\u0026langFirstLo, \u0026langFirstHi, c)\n\t\tsetBit(\u0026langRestLo, \u0026langRestHi, c)\n\t}\n\n\t// Bech HRP (when prefix==\"\"): [a-z], 1-16 chars.\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026bechHrpFirstLo, \u0026bechHrpFirstHi, c)\n\t\tsetBit(\u0026bechHrpRestLo, \u0026bechHrpRestHi, c)\n\t}\n\n\t// Bech data part: [a-z0-9], 6-90 chars.\n\tfor c := byte('a'); c \u003c= 'z'; c++ {\n\t\tsetBit(\u0026bechDataFirstLo, \u0026bechDataFirstHi, c)\n\t\tsetBit(\u0026bechDataRestLo, \u0026bechDataRestHi, c)\n\t}\n\tfor c := byte('0'); c \u003c= '9'; c++ {\n\t\tsetBit(\u0026bechDataFirstLo, \u0026bechDataFirstHi, c)\n\t\tsetBit(\u0026bechDataRestLo, \u0026bechDataRestHi, c)\n\t}\n}\n\nfunc setBit(lo, hi *uint64, c byte) {\n\tif c \u003c 64 {\n\t\t*lo |= 1 \u003c\u003c c\n\t} else {\n\t\t*hi |= 1 \u003c\u003c (c - 64)\n\t}\n}\n\n// UserName validates the r/sys/users-registration charset:\n// ^[a-z][a-z0-9]*([_-][a-z0-9]+)*$ length ≤ 64.\n//\n// The native MatchCharsetN enforces the leading-letter + tail-charset\n// shape and length bound; this helper also performs the bidi-strip\n// pre-pass. The \"no consecutive [_-]\" rule from r/sys/users is NOT\n// enforced here (it's a registration-policy rule, not a sanitization\n// concern — registrations go through r/sys/users itself).\n//\n// Returns the (bidi-stripped) input if valid, \"\" otherwise. On a \"\"\n// return, do not emit the user-mention markup at all (e.g. skip the\n// `[@user](/u/user)` link); falling back to the raw user-supplied\n// string would defeat the validation.\nfunc UserName(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif markdown.MatchCharsetN(s, userNameFirstLo, userNameFirstHi, userNameRestLo, userNameRestHi, 1, 64) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// BechString validates a bech32-style address-like string.\n//\n// A bech32 string has the shape `\u003chrp\u003e1\u003cdata\u003e`: a human-readable\n// prefix (HRP) that names the family (e.g. `g` for gno addresses,\n// `gpub` for gno pubkeys, `cosmos` for cosmos addresses), the\n// separator character `1`, then a data part carrying the encoded\n// payload as lowercase alphanumerics.\n//\n// If prefix != \"\", requires s to start with prefix+\"1\" exactly, and the\n// data part to match ^[a-z0-9]{6,90}$. Use this when you know the\n// expected family:\n//\n//\tsanitize.BechString(addr, \"g\")     // only g1...     (addresses)\n//\tsanitize.BechString(pk,   \"gpub\")  // only gpub1...  (pubkeys)\n//\n// If prefix == \"\", accepts any reasonable bech32 shape:\n// ^[a-z]{1,16}1[a-z0-9]{6,90}$.\n//\n// Syntactic only — does NOT verify the bech32 checksum. Use a true\n// bech32 decoder if you need that. Returns the cleaned input on\n// accept, \"\" on reject; on \"\" return, do not emit the address-link\n// markup (the user-supplied bytes have failed shape validation and\n// should not appear unmodified in output).\nfunc BechString(s, prefix string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif s == \"\" {\n\t\treturn \"\"\n\t}\n\tif prefix != \"\" {\n\t\t// HRP must be lowercase ASCII letters.\n\t\tfor i := 0; i \u003c len(prefix); i++ {\n\t\t\tc := prefix[i]\n\t\t\tif c \u003c 'a' || c \u003e 'z' {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t}\n\t\tneed := prefix + \"1\"\n\t\tif !strings.HasPrefix(s, need) {\n\t\t\treturn \"\"\n\t\t}\n\t\tdata := s[len(need):]\n\t\tif markdown.MatchCharsetN(data, bechDataFirstLo, bechDataFirstHi, bechDataRestLo, bechDataRestHi, 6, 90) {\n\t\t\treturn s\n\t\t}\n\t\treturn \"\"\n\t}\n\t// prefix == \"\" — accept any 1-16 char lowercase HRP, then '1', then data.\n\tsep := strings.IndexByte(s, '1')\n\tif sep \u003c 1 || sep \u003e 16 {\n\t\treturn \"\"\n\t}\n\thrp := s[:sep]\n\tif !markdown.MatchCharsetN(hrp, bechHrpFirstLo, bechHrpFirstHi, bechHrpRestLo, bechHrpRestHi, 1, 16) {\n\t\treturn \"\"\n\t}\n\tdata := s[sep+1:]\n\tif markdown.MatchCharsetN(data, bechDataFirstLo, bechDataFirstHi, bechDataRestLo, bechDataRestHi, 6, 90) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// FootnoteLabel validates an identifier used as a footnote name, link-\n// reference-definition label, or {#id} anchor: ^[A-Za-z0-9_-]{1,64}$.\n// Strips bidi/zero-width first. Returns s if valid, \"\" otherwise.\n//\n// Use for every shape where a markdown identifier is treated as an\n// opaque key by the parser:\n//\n//   - footnote-definition labels:        [^FootnoteLabel(name)]: body\n//   - footnote-reference labels:         see [^FootnoteLabel(name)]\n//   - link-reference-definition labels:  [FootnoteLabel(label)]: url\n//   - reference-link USE labels:         [text][FootnoteLabel(label)]\n//   - goldmark auto-anchor {#id}:        # Heading {#FootnoteLabel(id)}\n//\n// The shared validator name reflects the shared charset and shared\n// security goal — keep untrusted bytes out of any parser-managed\n// identifier slot.\n//\n// On \"\" return, omit the footnote / LRD / anchor entirely rather than\n// emitting it with raw user bytes.\nfunc FootnoteLabel(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif markdown.MatchCharsetN(s, footnoteLabelFirstLo, footnoteLabelFirstHi, footnoteLabelRestLo, footnoteLabelRestHi, 1, 64) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// LanguageName validates the language tag (a.k.a. \"info string\") for\n// a fenced code block — the `go` in:\n//\n//\t```go\n//\tfmt.Println(\"hi\")\n//\t```\n//\n// Charset: ^[a-zA-Z0-9_+-]{1,32}$ — letters, digits, `_`, `+`, `-`,\n// up to 32 bytes. Strips bidi/zero-width first.\n//\n// Returns the cleaned input if valid, \"\" otherwise. A \"\" return means\n// the caller should emit a language-less fence (``` without a tag)\n// rather than letting the user pick the syntax highlighter — which\n// could otherwise be used to inject newlines or block markers into\n// what becomes the opening fence line.\nfunc LanguageName(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tif markdown.MatchCharsetN(s, langFirstLo, langFirstHi, langRestLo, langRestHi, 1, 32) {\n\t\treturn s\n\t}\n\treturn \"\"\n}\n\n// NestedPrefix validates a prefix string for line-prefixing builders\n// like md.Nested, which prepends `prefix` to every line of content\n// to render the content as a nested/indented sub-block.\n//\n// Allowed: any string matching `^[ \\t\u003e]*$` — spaces, tabs, blockquote\n// `\u003e` chars only. Anything else (a `#`, a `-`, a letter) would let a\n// caller turn benign sub-content into a heading, list, or paragraph\n// at the wrong nesting level.\n//\n// Returns s if valid, \"\" otherwise. Strips bidi/zero-width first —\n// otherwise an invisible character hidden inside a `\u003e` prefix would\n// be replicated on every nested content line, producing per-line\n// display-vs-storage divergence.\n//\n// On \"\" return, fall back to a known-safe prefix literal (e.g.\n// `\"\u003e \"`) or skip the nesting entirely. Do not emit the raw\n// user-supplied prefix.\nfunc NestedPrefix(s string) string {\n\ts = markdown.StripBidiAndZeroWidth(s)\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c != ' ' \u0026\u0026 c != '\\t' \u0026\u0026 c != '\u003e' {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\treturn s\n}\n\n// ----- Primitive -----\n\n// CodeFence returns a string of backticks long enough to wrap content\n// as a CommonMark fenced code block without the content's own backticks\n// closing the fence prematurely.\n//\n// Returned length N = max(minCount, longestBacktickRunInContent + 1).\n// Use N backticks both before and after the content:\n//\n//\tfence := sanitize.CodeFence(userCode, 3)\n//\tout += fence + \"\\n\" + userCode + \"\\n\" + fence + \"\\n\"\n//\n// Typical minCount values:\n//   - 1 for inline code spans (`x`)\n//   - 3 for block fenced code (CommonMark §4.5 requires ≥3)\n//\n// `minCount \u003c 1` is clamped to 1. Empty content returns\n// strings.Repeat(\"`\", max(minCount, 1)). Never panics.\n//\n// Most realms should reach for InlineCode / CodeBlock /\n// LanguageCodeBlock below, which call CodeFence internally and emit\n// the full code block for you. Call CodeFence directly only when\n// you're rolling a custom fence emitter (e.g. a renderer that needs\n// the fence length but emits the body differently).\nfunc CodeFence(content string, minCount int) string {\n\treturn markdown.CodeFence(content, minCount)\n}\n\n// InlineCode wraps user content as a CommonMark inline code span — the\n// `code` in “ `code` “. Use for any user-derived token, identifier,\n// or short literal that should render in monospace inside running\n// prose: variable names, hashes, hex addresses, token symbols, error\n// codes, package paths, transaction IDs.\n//\n// Inline code spans cannot span lines (a `\\n` inside the content would\n// end the span and leave the surrounding backticks as literal text),\n// so all line breaks — CR / CRLF / LF, NEL (U+0085), U+2028, U+2029 —\n// are folded to a single space. If you want each line of user content\n// on its own row, use CodeBlock instead.\n//\n// Behavior:\n//   - Bidi/zero-width controls are stripped (browsers honor bidi marks\n//     inside `\u003ccode\u003e`, so leaving them would let stored bytes display\n//     as something different).\n//   - NUL is replaced with U+FFFD.\n//   - The wrapping fence is one backtick longer than the longest\n//     backtick run in the content, so internal backticks can never\n//     close the span prematurely.\n//   - A single space pad is added on each side when content starts or\n//     ends with “ ` “ or space, so leading/trailing backticks render\n//     literally rather than fusing with the fence (the renderer\n//     strips one space from each side per CommonMark spec).\n//\n// Empty input returns \"\" rather than a literal two-backtick string\n// (which CommonMark parses as text, not as an empty code span). If\n// you use InlineCode as link text and it returns \"\", omit the link\n// entirely.\n//\n// Not idempotent (see package doc): wraps with a fence, so calling\n// twice double-wraps.\nfunc InlineCode(content string) string {\n\tcontent = markdown.StripBidiAndZeroWidth(content)\n\tcontent = markdown.NormalizeBreaks(content)\n\tcontent = foldNewlinesAndSeparators(content, ' ')\n\tcontent = replaceNULWithFFFD(content)\n\tif content == \"\" {\n\t\treturn \"\"\n\t}\n\tfence := markdown.CodeFence(content, 1)\n\tpad := \"\"\n\tif content[0] == '`' || content[0] == ' ' ||\n\t\tcontent[len(content)-1] == '`' || content[len(content)-1] == ' ' {\n\t\tpad = \" \"\n\t}\n\treturn fence + pad + content + pad + fence\n}\n\n// CodeBlock wraps user content as a CommonMark fenced code block.\n// Use for any user-derived multi-line snippet that should render as a\n// code block: log excerpts, JSON dumps, error backtraces, config\n// snippets, posted code samples.\n//\n// Behavior:\n//   - Bidi/zero-width controls are stripped.\n//   - CR/CRLF line endings are normalized to LF; Unicode separators\n//     (NEL U+0085, U+2028, U+2029) are folded to LF for line-count\n//     consistency.\n//   - NUL is replaced with U+FFFD per CM §2.3.\n//   - The wrapping fence is at least 3 backticks (CM §4.5 minimum) and\n//     sized to outscan internal backticks — an attacker cannot embed\n//     a closing fence in the content.\n//\n// Empty content emits an empty fenced block (\"```\\n\\n```\\n\"), which is\n// valid CommonMark and renders as an empty `\u003cpre\u003e\u003ccode\u003e\u003c/code\u003e\u003c/pre\u003e`.\n//\n// Not idempotent (see package doc).\nfunc CodeBlock(content string) string {\n\tcontent = markdown.StripBidiAndZeroWidth(content)\n\tcontent = markdown.NormalizeBreaks(content)\n\tcontent = foldNewlinesAndSeparators(content, '\\n')\n\tcontent = replaceNULWithFFFD(content)\n\tfence := markdown.CodeFence(content, 3)\n\treturn fence + \"\\n\" + content + \"\\n\" + fence + \"\\n\"\n}\n\n// LanguageCodeBlock wraps user content as a fenced code block tagged\n// with a programming-language hint (the \"info string\" after the\n// opening fence, e.g. `go` in ```` ```go ````) so the renderer can\n// apply syntax highlighting.\n//\n// An invalid `language` tag silently falls back to a tagless fence —\n// the helper never returns an error or panics. If a realm author is\n// debugging \"why is my Go highlighting gone?\", the input failed the\n// language validator (charset ^[a-zA-Z0-9_+-]{1,32}$ after bidi-strip).\n// This fallback exists because an unvalidated tag could contain a\n// newline that injects content (e.g. a heading) onto what becomes the\n// opening fence line.\n//\n// Content is cleaned exactly as in CodeBlock (bidi-strip, CR/CRLF\n// normalize to LF, NEL/U+2028/U+2029 fold to LF, NUL→U+FFFD, fence\n// sized to outscan internal backticks).\n//\n// Not idempotent (see package doc).\nfunc LanguageCodeBlock(language, content string) string {\n\tcontent = markdown.StripBidiAndZeroWidth(content)\n\tcontent = markdown.NormalizeBreaks(content)\n\tcontent = foldNewlinesAndSeparators(content, '\\n')\n\tcontent = replaceNULWithFFFD(content)\n\tfence := markdown.CodeFence(content, 3)\n\tlang := LanguageName(language) // \"\" on reject\n\treturn fence + lang + \"\\n\" + content + \"\\n\" + fence + \"\\n\"\n}\n\n// ----- Reference-style definitions -----\n\n// FootnoteDefinition emits a GFM footnote definition — the\n// `[^name]: body` form that introduces a footnote whose body is rendered\n// in the page footer (or wherever the renderer chooses to place it).\n// Other parts of the markdown reference the footnote by writing\n// `[^name]` inline.\n//\n// Use for any realm-rendered footnote where the body text comes from\n// user input. The realm picks the footnote name (passed as `name`,\n// validated by FootnoteLabel — failure here returns \"\"); the user's\n// content goes in `text`, which is sanitized via Block.\n//\n// Contract:\n//   - `name`: passed raw, validated as a FootnoteLabel\n//     (^[A-Za-z0-9_-]{1,64}$). Reject → return \"\".\n//   - `text`: passed raw multi-paragraph user prose, cleaned via Block\n//     (bidi-strip, line-ending normalize, LRD strip, block-marker\n//     escape, ref-link USE escape, fence auto-close).\n//\n// Empty body → returns \"\" (a label without body is not a valid\n// footnote definition; the markdown would parse as a paragraph\n// containing the label).\n//\n// Output shape:\n//\n//\t[^name]:\n//\t    line 1 of body\n//\t    line 2 of body\n//\t    ...\n//\n// The label sits on its own line and each body line gets a 4-space\n// indent — the GFM continuation rule that keeps multi-paragraph body\n// text bound to the footnote rather than detaching as a new paragraph.\n//\n// Not idempotent (see package doc): composes Block internally; passing\n// already-sanitized body text double-escapes.\nfunc FootnoteDefinition(name, text string) string {\n\tlabel := FootnoteLabel(name)\n\tif label == \"\" {\n\t\treturn \"\"\n\t}\n\t// Block now wraps with \"\\n\\n\" on both sides for cross-paragraph\n\t// isolation; inside a footnote-definition's 4-space-indented body\n\t// the wrap would line-prefix to blank padding lines, so strip ALL\n\t// leading and trailing \"\\n\"s before continuation-indenting.\n\tbody := strings.Trim(Block(text), \"\\n\")\n\tif body == \"\" {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[^\")\n\tb.WriteString(label)\n\tb.WriteString(\"]:\\n\")\n\tfor _, line := range strings.Split(body, \"\\n\") {\n\t\tif line == \"\" {\n\t\t\tb.WriteByte('\\n')\n\t\t} else {\n\t\t\tb.WriteString(\"    \")\n\t\t\tb.WriteString(line)\n\t\t\tb.WriteByte('\\n')\n\t\t}\n\t}\n\treturn b.String()\n}\n\n// LinkReferenceDefinition emits a CommonMark link reference definition\n// (CM §4.7) — the `[label]: url \"title\"` form that other parts of the\n// markdown reference by writing `[text][label]` or `[label]` (shortcut).\n//\n// Use for any realm-rendered LRD where the realm owns the label but\n// any of the URL or title come from user input. The user content for\n// the URL goes through URL (allowlist-based — reject → \"\"); the title\n// goes through LinkTitle (escape).\n//\n// Contract:\n//   - `label`: passed raw, validated as a FootnoteLabel\n//     (^[A-Za-z0-9_-]{1,64}$). Realms should choose a namespaced label\n//     using dashes (e.g. `r-myrealm-help`) so shortcut-reference\n//     invocations from user content can't collide with bare prose\n//     (`[help]`, `[click here]`). `/` is not in the FootnoteLabel\n//     charset; reject → return \"\".\n//   - `url`: passed raw, sanitized via URL. If URL rejects, the LRD is\n//     skipped (return \"\").\n//   - `title`: passed raw, sanitized via LinkTitle. Empty title → no\n//     title clause emitted.\n//\n// The output is framed with leading and trailing blank lines so that\n// the definition cannot accidentally fuse with adjacent paragraph\n// content into a setext underline or a continuation line.\n//\n// Not idempotent (see package doc).\nfunc LinkReferenceDefinition(label, url, title string) string {\n\tlbl := FootnoteLabel(label)\n\tif lbl == \"\" {\n\t\treturn \"\"\n\t}\n\tsafeURL := URL(url)\n\tif safeURL == \"\" {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"\\n\\n[\")\n\tb.WriteString(lbl)\n\tb.WriteString(\"]: \")\n\tb.WriteString(safeURL)\n\tif title != \"\" {\n\t\tb.WriteString(\" \\\"\")\n\t\tb.WriteString(LinkTitle(title))\n\t\tb.WriteString(\"\\\"\")\n\t}\n\tb.WriteString(\"\\n\\n\")\n\treturn b.String()\n}\n\n// ----- internal helpers -----\n\n// linkSchemeAllowed returns true if s passes the URL helper's scheme\n// allowlist. See URL's doc for the policy.\nfunc linkSchemeAllowed(s string) bool {\n\tif strings.HasPrefix(s, \"http://\") || strings.HasPrefix(s, \"https://\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"mailto:\") {\n\t\t// Reject any query: RFC 6068 headers (body, subject, cc, bcc, ...)\n\t\t// prefill the composed message and are a phishing vector. '?' opens\n\t\t// the header section (covering percent-encoded names like ?%62ody=).\n\t\t// '\u0026' is rejected because gnoweb's renderer decodes HTML character\n\t\t// references ('\u0026#63;', '\u0026#x3f;', '\u0026quest;') back into '?' after this\n\t\t// check, reconstituting a query. Percent-encoded '%3f' stays encoded\n\t\t// and reads as a literal '?' in the address, so it's allowed.\n\t\tif strings.ContainsAny(s, \"?\u0026\") {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"//\") {\n\t\t// Protocol-relative — reject (tracking-pixel vector).\n\t\treturn false\n\t}\n\t// Any URL with an unknown scheme (RFC 3986: `^[a-zA-Z][a-zA-Z0-9+.-]*:`)\n\t// is rejected — this blocks `javascript:`, `data:`, `vbscript:`, `blob:`,\n\t// and anything else not handled above. URLs without a scheme are\n\t// treated as relative and accepted (bare path, query-only, fragment).\n\tif hasURLScheme(s) {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// hasURLScheme reports whether s begins with a scheme followed by ':'\n// per RFC 3986 (^[a-zA-Z][a-zA-Z0-9+.-]*:). A `:` appearing later in\n// the URL (e.g. `/path:foo` or `?q=a:b`) does not count.\nfunc hasURLScheme(s string) bool {\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\tc := s[0]\n\tif !((c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z')) {\n\t\treturn false\n\t}\n\tfor i := 1; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c == ':' {\n\t\t\treturn true\n\t\t}\n\t\tif !((c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n\t\t\t(c \u003e= '0' \u0026\u0026 c \u003c= '9') || c == '+' || c == '.' || c == '-') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// imageSchemeAllowed returns true if s passes the ImageURL helper's\n// scheme allowlist. Tighter than linkSchemeAllowed: no mailto/tel,\n// only data:image/\u003csubset\u003e.\nfunc imageSchemeAllowed(s string) bool {\n\tif strings.HasPrefix(s, \"http://\") || strings.HasPrefix(s, \"https://\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"//\") {\n\t\treturn false\n\t}\n\tif strings.HasPrefix(s, \"/\") || strings.HasPrefix(s, \"./\") || strings.HasPrefix(s, \"../\") {\n\t\treturn true\n\t}\n\tif strings.HasPrefix(s, \"data:\") {\n\t\t// Only the curated image/* subset. CSS must enforce sizing.\n\t\tfor _, p := range []string{\n\t\t\t\"data:image/svg+xml\",\n\t\t\t\"data:image/png\",\n\t\t\t\"data:image/jpeg\",\n\t\t\t\"data:image/gif\",\n\t\t\t\"data:image/webp\",\n\t\t} {\n\t\t\tif strings.HasPrefix(s, p) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\treturn false\n}\n\n// foldNewlinesAndSeparators replaces \\n, U+0085 NEL, U+2028 LINE SEPARATOR,\n// U+2029 PARAGRAPH SEPARATOR with the given replacement byte (typically\n// space for inline-context helpers).\n//\n// NormalizeBreaks has already folded \\r\\n and \\r to \\n before this runs,\n// so \\n is the canonical break byte to substitute.\nfunc foldNewlinesAndSeparators(s string, replacement byte) string {\n\tif !needsSeparatorFold(s) {\n\t\treturn s\n\t}\n\tout := make([]byte, 0, len(s))\n\tfor i := 0; i \u003c len(s); {\n\t\tc := s[i]\n\t\tif c == '\\n' {\n\t\t\tout = append(out, replacement)\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\t\t// U+0085 NEL: 0xC2 0x85\n\t\tif c == 0xC2 \u0026\u0026 i+1 \u003c len(s) \u0026\u0026 s[i+1] == 0x85 {\n\t\t\tout = append(out, replacement)\n\t\t\ti += 2\n\t\t\tcontinue\n\t\t}\n\t\t// U+2028 (0xE2 0x80 0xA8) or U+2029 (0xE2 0x80 0xA9)\n\t\tif c == 0xE2 \u0026\u0026 i+2 \u003c len(s) \u0026\u0026 s[i+1] == 0x80 \u0026\u0026 (s[i+2] == 0xA8 || s[i+2] == 0xA9) {\n\t\t\tout = append(out, replacement)\n\t\t\ti += 3\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, c)\n\t\ti++\n\t}\n\treturn string(out)\n}\n\nfunc needsSeparatorFold(s string) bool {\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c == '\\n' || c == 0xC2 || c == 0xE2 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// replaceNULWithFFFD substitutes any NUL byte with the UTF-8 encoding\n// of U+FFFD REPLACEMENT CHARACTER per CM §2.3.\nfunc replaceNULWithFFFD(s string) string {\n\tif !strings.ContainsRune(s, 0) {\n\t\treturn s\n\t}\n\treturn strings.ReplaceAll(s, \"\\x00\", \"\\ufffd\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"md","path":"gno.land/p/moul/md","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/md\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"md.gno","body":"// Package md provides helper functions for generating Markdown content programmatically.\n//\n// It includes utilities for text formatting, creating lists, blockquotes, code blocks,\n// links, images, and more.\n//\n// Highlights:\n// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists.\n// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists).\n// - Includes advanced helpers like inline images with links and nested list prefixes.\n//\n// For a comprehensive example of how to use these helpers, see:\n// https://gno.land/r/docs/moul_md\n//\n// # Sanitization contract\n//\n// Some helpers in this package sanitize their user-derived arguments\n// INTERNALLY (via p/nt/markdown/sanitize/v0). When using these, pass\n// raw user input — do NOT pre-wrap with sanitize.*, or you will\n// double-wrap (the escapers are not idempotent and double-wrap is a\n// bug):\n//\n//\tLink, UserLink, Image, InlineImageWithLink, FootnoteDefinition,\n//\tLinkReferenceDefinition, CollapsibleSection (title only),\n//\tInlineCode, CodeBlock, LanguageCodeBlock, Blockquote\n//\n// The other helpers DO NOT sanitize — they are pure builders that\n// wrap their input in markdown chrome. User-derived input reaches\n// the output unmodified, so callers MUST wrap with sanitize.* at the\n// call site:\n//\n//\tBold, Italic, Strikethrough, H1-H6, BulletList, BulletItem,\n//\tOrderedList, TodoList, TodoItem, Nested, Paragraph, Columns,\n//\tColumnsN, HorizontalRule\n//\n// Examples:\n//\n//\t// Sanitizing helper — pass raw:\n//\tout += md.Link(post.Title, post.URL)                                 // good\n//\tout += md.Link(sanitize.InlineText(post.Title), sanitize.URL(post.URL)) // BAD: double-wrap\n//\n//\t// Non-sanitizing helper — wrap once:\n//\tout += md.H2(sanitize.InlineText(post.Title))                        // good\n//\tout += md.H2(post.Title)                                             // BAD: raw user input\n//\tout += md.H2(sanitize.InlineText(sanitize.InlineText(post.Title)))   // BAD: double-wrap\n//\n// Composition: outputs of sanitizing helpers are safe markdown chrome\n// and can be embedded inside non-sanitizing helpers freely:\n//\n//\tout += md.H2(md.Link(post.Title, post.URL))   // good — H2 doesn't re-escape Link's output\n//\n// The reverse is unsafe: do NOT embed a non-sanitizing helper's\n// markdown chrome inside a sanitizing helper's arg, or the inner\n// markdown gets re-escaped:\n//\n//\tout += md.Link(md.Bold(post.Title), post.URL) // BAD: Link's internal sanitize\n//\t                                              // escapes the ** chars from md.Bold\npackage md\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/markdown/sanitize/v0\"\n)\n\n// Bold returns bold text for markdown.\n// Example: Bold(\"foo\") =\u003e \"**foo**\"\nfunc Bold(text string) string {\n\treturn \"**\" + text + \"**\"\n}\n\n// Italic returns italicized text for markdown.\n// Example: Italic(\"foo\") =\u003e \"*foo*\"\nfunc Italic(text string) string {\n\treturn \"*\" + text + \"*\"\n}\n\n// Strikethrough returns strikethrough text for markdown.\n// Example: Strikethrough(\"foo\") =\u003e \"~~foo~~\"\nfunc Strikethrough(text string) string {\n\treturn \"~~\" + text + \"~~\"\n}\n\n// H1 returns a level 1 header for markdown.\n// Example: H1(\"foo\") =\u003e \"# foo\\n\"\nfunc H1(text string) string {\n\treturn \"# \" + text + \"\\n\"\n}\n\n// H2 returns a level 2 header for markdown.\n// Example: H2(\"foo\") =\u003e \"## foo\\n\"\nfunc H2(text string) string {\n\treturn \"## \" + text + \"\\n\"\n}\n\n// H3 returns a level 3 header for markdown.\n// Example: H3(\"foo\") =\u003e \"### foo\\n\"\nfunc H3(text string) string {\n\treturn \"### \" + text + \"\\n\"\n}\n\n// H4 returns a level 4 header for markdown.\n// Example: H4(\"foo\") =\u003e \"#### foo\\n\"\nfunc H4(text string) string {\n\treturn \"#### \" + text + \"\\n\"\n}\n\n// H5 returns a level 5 header for markdown.\n// Example: H5(\"foo\") =\u003e \"##### foo\\n\"\nfunc H5(text string) string {\n\treturn \"##### \" + text + \"\\n\"\n}\n\n// H6 returns a level 6 header for markdown.\n// Example: H6(\"foo\") =\u003e \"###### foo\\n\"\nfunc H6(text string) string {\n\treturn \"###### \" + text + \"\\n\"\n}\n\n// BulletList returns a bullet list for markdown.\n// Example: BulletList([]string{\"foo\", \"bar\"}) =\u003e \"- foo\\n- bar\\n\"\nfunc BulletList(items []string) string {\n\tvar sb strings.Builder\n\tfor _, item := range items {\n\t\tsb.WriteString(BulletItem(item))\n\t}\n\treturn sb.String()\n}\n\n// BulletItem returns a bullet item for markdown.\n// Example: BulletItem(\"foo\") =\u003e \"- foo\\n\"\nfunc BulletItem(item string) string {\n\tvar sb strings.Builder\n\tlines := strings.Split(item, \"\\n\")\n\tsb.WriteString(\"- \" + lines[0] + \"\\n\")\n\tfor _, line := range lines[1:] {\n\t\tsb.WriteString(\"  \" + line + \"\\n\")\n\t}\n\treturn sb.String()\n}\n\n// OrderedList returns an ordered list for markdown.\n// Example: OrderedList([]string{\"foo\", \"bar\"}) =\u003e \"1. foo\\n2. bar\\n\"\nfunc OrderedList(items []string) string {\n\tvar sb strings.Builder\n\tfor i, item := range items {\n\t\tlines := strings.Split(item, \"\\n\")\n\t\tsb.WriteString(strconv.Itoa(i+1) + \". \" + lines[0] + \"\\n\")\n\t\tfor _, line := range lines[1:] {\n\t\t\tsb.WriteString(\"   \" + line + \"\\n\")\n\t\t}\n\t}\n\treturn sb.String()\n}\n\n// TodoList returns a list of todo items with checkboxes for markdown.\n// Example: TodoList([]string{\"foo\", \"bar\\nmore bar\"}, []bool{true, false}) =\u003e \"- [x] foo\\n- [ ] bar\\n  more bar\\n\"\nfunc TodoList(items []string, done []bool) string {\n\tvar sb strings.Builder\n\tfor i, item := range items {\n\t\tsb.WriteString(TodoItem(item, done[i]))\n\t}\n\treturn sb.String()\n}\n\n// TodoItem returns a todo item with checkbox for markdown.\n// Example: TodoItem(\"foo\", true) =\u003e \"- [x] foo\\n\"\nfunc TodoItem(item string, done bool) string {\n\tvar sb strings.Builder\n\tcheckbox := \" \"\n\tif done {\n\t\tcheckbox = \"x\"\n\t}\n\tlines := strings.Split(item, \"\\n\")\n\tsb.WriteString(\"- [\" + checkbox + \"] \" + lines[0] + \"\\n\")\n\tfor _, line := range lines[1:] {\n\t\tsb.WriteString(\"  \" + line + \"\\n\")\n\t}\n\treturn sb.String()\n}\n\n// Nested prefixes each line with a given prefix, enabling nested lists.\n// Example: Nested(\"- foo\\n- bar\", \"  \") =\u003e \"  - foo\\n  - bar\\n\"\nfunc Nested(content, prefix string) string {\n\tlines := strings.Split(content, \"\\n\")\n\tfor i := range lines {\n\t\tif strings.TrimSpace(lines[i]) != \"\" {\n\t\t\tlines[i] = prefix + lines[i]\n\t\t}\n\t}\n\treturn strings.Join(lines, \"\\n\")\n}\n\n// Blockquote returns the text as a CommonMark blockquote.\n// Example: Blockquote(\"foo\\nbar\") =\u003e \"\\n\u003e foo\\n\u003e bar\\n\\n\"\n//\n// Delegates to sanitize.Blockquote, which cleans the content (bidi-strip,\n// CR/CRLF/U+2028/U+2029/NEL line-ending normalize, LRD strip, ref-link\n// escape, block-marker escape, fence auto-close), line-prefixes each\n// line with \"\u003e \", and wraps with \"\\n\" / \"\\n\\n\" so the quote opens\n// cleanly and cannot pull appended chrome into the quote via CM §5.2\n// lazy continuation. Callers do NOT need to pre-wrap the input.\nfunc Blockquote(text string) string {\n\treturn sanitize.Blockquote(text)\n}\n\n// InlineCode wraps the given text as a CommonMark inline code span.\n// Example: InlineCode(\"foo\") =\u003e \"`foo`\"\n//\n// Delegates to sanitize.InlineCode, which cleans the input (bidi-strip,\n// CR/CRLF + NEL + U+2028/U+2029 folded to single space, NUL→U+FFFD)\n// and picks a backtick-run length that outscans any internal backticks.\n// Callers do NOT need to pre-wrap the input.\nfunc InlineCode(code string) string {\n\treturn sanitize.InlineCode(code)\n}\n\n// CodeBlock creates a markdown code block.\n// Example: CodeBlock(\"foo\") =\u003e \"```\\nfoo\\n```\"\n//\n// Delegates to sanitize.CodeBlock, which cleans the content (bidi-strip,\n// CR/CRLF normalize, NEL/U+2028/U+2029 fold, NUL→U+FFFD) and picks a\n// fence wide enough to outscan any backticks in content. Callers do NOT\n// need to pre-wrap content with another sanitize helper.\nfunc CodeBlock(content string) string {\n\treturn sanitize.CodeBlock(content)\n}\n\n// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting.\n// Example: LanguageCodeBlock(\"go\", \"foo\") =\u003e \"```go\\nfoo\\n```\"\n//\n// Delegates to sanitize.LanguageCodeBlock, which validates the language\n// tag (charset ^[a-zA-Z0-9_+-]{1,32}$, falling back to a tagless fence\n// if invalid) and cleans the content as CodeBlock does. Callers do NOT\n// need to pre-wrap either argument.\nfunc LanguageCodeBlock(language, content string) string {\n\treturn sanitize.LanguageCodeBlock(language, content)\n}\n\n// HorizontalRule returns a horizontal rule for markdown.\n// Example: HorizontalRule() =\u003e \"---\\n\"\nfunc HorizontalRule() string {\n\treturn \"---\\n\"\n}\n\n// Link returns a hyperlink for markdown.\n// Example: Link(\"foo\", \"http://example.com\") =\u003e \"[foo](http://example.com)\"\n//\n// The text and url args are sanitized internally — text via\n// sanitize.InlineText, url via sanitize.URL (allowlists http/https/\n// mailto/relative/fragment; rejects javascript:, data:, blob:, etc.).\n// Callers do NOT need to pre-wrap either argument; double-wrapping is\n// a bug (the inline-text escaper is non-idempotent).\n//\n// If the URL fails the scheme allowlist, the href is rendered empty —\n// the link becomes inert rather than carrying a malicious destination.\nfunc Link(text, url string) string {\n\treturn \"[\" + sanitize.InlineText(text) + \"](\" + sanitize.URL(url) + \")\"\n}\n\n// UserLink returns a user profile link for markdown.\n// Example: UserLink(\"moul\") =\u003e \"[@moul](/u/moul)\"\n// Example: UserLink(\"g1blah...\") =\u003e \"[g1blah...](/u/g1blah...)\"\n//\n// Validates the user identifier — if it matches the gno bech32 address\n// pattern (g1...), produces an address-style link; otherwise tries the\n// r/sys/users-charset username and produces an @-style link. Returns\n// \"\" if the identifier matches neither — callers should treat \"\" as\n// \"skip the user mention\" rather than emit a broken link.\nfunc UserLink(user string) string {\n\tif addr := sanitize.BechString(user, \"g\"); addr != \"\" {\n\t\treturn \"[\" + addr + \"](/u/\" + addr + \")\"\n\t}\n\tif name := sanitize.UserName(user); name != \"\" {\n\t\treturn \"[@\" + name + \"](/u/\" + name + \")\"\n\t}\n\treturn \"\"\n}\n\n// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown.\n// Example: InlineImageWithLink(\"alt text\", \"image-url\", \"link-url\") =\u003e \"[![alt text](image-url)](link-url)\"\n//\n// altText and imageUrl are sanitized via Image (sanitize.InlineText +\n// sanitize.ImageURL); linkUrl is sanitized via sanitize.URL. Callers\n// do NOT need to pre-wrap any argument.\nfunc InlineImageWithLink(altText, imageUrl, linkUrl string) string {\n\treturn \"[\" + Image(altText, imageUrl) + \"](\" + sanitize.URL(linkUrl) + \")\"\n}\n\n// Image returns an image for markdown.\n// Example: Image(\"foo\", \"http://example.com\") =\u003e \"![foo](http://example.com)\"\n//\n// altText is sanitized via sanitize.InlineText, url via\n// sanitize.ImageURL (allowlists http/https/relative + data:image/*;\n// rejects mailto:, javascript:, data:text/html, etc.). Callers do NOT\n// need to pre-wrap either argument.\nfunc Image(altText, url string) string {\n\treturn \"![\" + sanitize.InlineText(altText) + \"](\" + sanitize.ImageURL(url) + \")\"\n}\n\n// FootnoteDefinition emits a GFM footnote definition — `[^name]: body` —\n// for a footnote that is referenced elsewhere in the document by\n// `[^name]`.\n//\n// Example: FootnoteDefinition(\"note1\", \"Long form of the citation.\")\n// renders as:\n//\n//\t[^note1]:\n//\t    Long form of the citation.\n//\n// The `name` is validated as a FootnoteLabel (^[A-Za-z0-9_-]{1,64}$);\n// `text` is user-supplied multi-paragraph prose, sanitized via Block.\n// An invalid name or empty body returns \"\".\n//\n// Delegates to sanitize.FootnoteDefinition. Callers do NOT need to\n// pre-wrap either argument.\nfunc FootnoteDefinition(name, text string) string {\n\treturn sanitize.FootnoteDefinition(name, text)\n}\n\n// LinkReferenceDefinition emits a CommonMark link reference definition\n// (CM §4.7) — `[label]: url \"title\"` — for a reference link that is\n// invoked elsewhere by `[text][label]` or by the shortcut form\n// `[label]`.\n//\n// Example: LinkReferenceDefinition(\"r/docs/help\", \"/r/docs/help\", \"\")\n// renders as:\n//\n//\t[r/docs/help]: /r/docs/help\n//\n// The `label` is validated as a FootnoteLabel (^[A-Za-z0-9_-]{1,64}$);\n// `url` is sanitized via URL (allowlist); `title` is sanitized via\n// LinkTitle. An invalid label or rejected URL returns \"\".\n//\n// Realms should choose a namespaced label using dashes\n// (e.g. `r-myrealm-help`) so that shortcut-reference invocations from\n// user content can't collide with bare words a user is likely to write.\n// `/` is not in the FootnoteLabel charset.\n//\n// Delegates to sanitize.LinkReferenceDefinition. Callers do NOT need to\n// pre-wrap any argument.\nfunc LinkReferenceDefinition(label, url, title string) string {\n\treturn sanitize.LinkReferenceDefinition(label, url, title)\n}\n\n// Paragraph wraps the given text in a Markdown paragraph.\n// Example: Paragraph(\"foo\") =\u003e \"foo\\n\"\nfunc Paragraph(content string) string {\n\treturn content + \"\\n\\n\"\n}\n\n// CollapsibleSection creates a collapsible section for markdown using\n// HTML \u003cdetails\u003e and \u003csummary\u003e tags.\n// Example:\n// CollapsibleSection(\"Click to expand\", \"Hidden content\")\n// =\u003e\n// \u003cdetails\u003e\u003csummary\u003eClick to expand\u003c/summary\u003e\n//\n// Hidden content\n// \u003c/details\u003e\n//\n// The title argument is sanitized via sanitize.HTMLEscape (it lands in\n// an HTML element body inside \u003csummary\u003e, not a markdown context — so\n// HTML entity escaping is the correct policy, not markdown backslash\n// escaping). The content argument is passed through unchanged because\n// \u003cdetails\u003e with a blank-line-separated body allows markdown inside\n// (CM §4.6); callers must pre-wrap content with sanitize.Block if it\n// derives from user input.\nfunc CollapsibleSection(title, content string) string {\n\treturn \"\u003cdetails\u003e\u003csummary\u003e\" + sanitize.HTMLEscape(title) + \"\u003c/summary\u003e\\n\\n\" + content + \"\\n\u003c/details\u003e\\n\"\n}\n\n// EscapeURL escapes characters in a URL for use in markdown link syntax.\n//\n// Deprecated: use sanitize.URL (for link href) or sanitize.ImageURL\n// (for image src) directly. EscapeURL previously only percent-encoded\n// ( and ) and did not validate the URL scheme — a security footgun\n// (EscapeURL(\"javascript:alert(1)\") returned a working XSS payload).\n// It now delegates to sanitize.URL, which allowlists schemes (rejects\n// javascript:, data:text/html, vbscript:, blob:, etc.) and\n// percent-encodes all unsafe bytes (including the ( ) this function\n// handled). The other helpers in this package (Link, UserLink, Image,\n// InlineImageWithLink) sanitize their URL args internally now, so you\n// rarely need to call any URL-escape helper directly.\n//\n// Behavior change vs. the original implementation: invalid schemes\n// now return \"\" instead of passing through with ( ) escaped. Non-ASCII\n// bytes get percent-encoded to standard RFC 3986 wire form instead of\n// passing through as raw UTF-8.\nfunc EscapeURL(url string) string {\n\treturn sanitize.URL(url)\n}\n\n// EscapeText escapes special Markdown characters in regular text for\n// use in inline contexts.\n//\n// Deprecated: use sanitize.InlineText directly. EscapeText was\n// INCOMPLETE — it missed \\, #, \u003c, \u0026 and did not strip bidi/zero-width\n// characters, replace NUL with U+FFFD, or normalize line endings.\n// User input could inject backslash escapes (\\* cancels neighboring\n// escapes), autolinks (\u003chttps://x\u003e), raw HTML (\u003cscript\u003e), HTML entity\n// references (\u0026amp;), and bidi spoofing (RLO before an address). It\n// now delegates to sanitize.InlineText. The other helpers in this\n// package (Link, UserLink, Image, InlineImageWithLink, CollapsibleSection)\n// sanitize their text args internally now, so you rarely need to call\n// any inline-text-escape helper directly.\n//\n// Behavior change vs. the original implementation: \\, #, \u003c, \u0026 are now\n// escaped; | is no longer escaped (the original was over-escaping —\n// outside GFM table-cell context, | is markdown-inert; for table\n// cells use sanitize.TableCell which adds the | escape on top of the\n// inline-text set). Bidi controls are stripped, NUL becomes U+FFFD,\n// and line endings normalize.\nfunc EscapeText(text string) string {\n\treturn sanitize.InlineText(text)\n}\n\n// Columns returns a formatted row of columns using the Gno syntax.\n// If you want a specific number of columns per row (\u003c=4), use ColumnsN.\n// Check /r/docs/markdown#columns for more info.\n// If padded=true \u0026 the final \u003cgno-columns\u003e tag is missing column content, an empty\n// column element will be placed to keep the cols per row constant.\n// Padding works only with colsPerRow \u003e 0.\n//\n// Example:\n//\n//\tColumns([]string{\"A\", \"B\"}, false)\n//\t// Returns:\n//\t// \u003cgno-columns\u003e\n//\t// A\n//\t// \u003cgno-columns-sep\u003e\n//\t// B\n//\t// \u003c/gno-columns\u003e\nfunc Columns(contentByColumn []string, padded bool) string {\n\tif len(contentByColumn) == 0 {\n\t\treturn \"\"\n\t}\n\tmaxCols := 4\n\tif padded \u0026\u0026 len(contentByColumn)%maxCols != 0 {\n\t\tmissing := maxCols - len(contentByColumn)%maxCols\n\t\tcontentByColumn = append(contentByColumn, make([]string, missing)...)\n\t}\n\n\tvar sb strings.Builder\n\tsb.WriteString(\"\u003cgno-columns\u003e\\n\")\n\n\tfor i, column := range contentByColumn {\n\t\tif i \u003e 0 {\n\t\t\tsb.WriteString(\"\u003cgno-columns-sep\u003e\\n\")\n\t\t}\n\t\tsb.WriteString(column + \"\\n\")\n\t}\n\n\tsb.WriteString(\"\u003c/gno-columns\u003e\\n\")\n\treturn sb.String()\n}\n\nconst maxColumnsPerRow = 4\n\n// ColumnsN splits content into multiple rows of N columns each and formats them.\n// If colsPerRow \u003c= 0, all items are placed in one \u003cgno-columns\u003e block.\n// If padded=true \u0026 the final \u003cgno-columns\u003e tag is missing column content, an empty\n// column element will be placed to keep the cols per row constant.\n// Padding works only with colsPerRow \u003e 0.\n// Note: On standard-size screens, gnoweb handles a max of 4 cols per row.\n//\n// Example:\n//\n//\tColumnsN([]string{\"A\", \"B\", \"C\"}, 2, false)\n//\t// Returns:\n//\t// \u003cgno-columns\u003e\n//\t// A\n//\t// \u003cgno-columns-sep\u003e\n//\t// B\n//\t// \u003c/gno-columns\u003e\n//\t// \u003cgno-columns\u003e\n//\t// C\n//\t// \u003c/gno-columns\u003e\nfunc ColumnsN(content []string, colsPerRow int, padded bool) string {\n\tif len(content) == 0 {\n\t\treturn \"\"\n\t}\n\tif colsPerRow \u003c= 0 {\n\t\treturn Columns(content, padded)\n\t}\n\n\tvar sb strings.Builder\n\t// Case 2: Multiple blocks with max 4 columns\n\tfor i := 0; i \u003c len(content); i += colsPerRow {\n\t\tend := i + colsPerRow\n\t\tif end \u003e len(content) {\n\t\t\tend = len(content)\n\t\t}\n\t\trow := content[i:end]\n\n\t\t// Add padding if needed\n\t\tif padded \u0026\u0026 len(row) \u003c colsPerRow {\n\t\t\trow = append(row, make([]string, colsPerRow-len(row))...)\n\t\t}\n\n\t\tsb.WriteString(Columns(row, false))\n\t}\n\treturn sb.String()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"rotree","path":"gno.land/p/nt/avl/v0/rotree","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/v0/rotree\"\ngno = \"0.9\"\n"},{"name":"rotree.gno","body":"// Package rotree provides a read-only wrapper for avl.Tree with safe value transformation.\n//\n// It is useful when you want to expose a read-only view of a tree while ensuring that\n// the sensitive data cannot be modified.\n//\n// Example:\n//\n//\t// Define a user structure with sensitive data\n//\ttype User struct {\n//\t\tName     string\n//\t\tBalance  int\n//\t\tInternal string // sensitive field\n//\t}\n//\n//\t// Create and populate the original tree\n//\tprivateTree := avl.NewTree()\n//\tprivateTree.Set(\"alice\", \u0026User{\n//\t\tName:     \"Alice\",\n//\t\tBalance:  100,\n//\t\tInternal: \"sensitive\",\n//\t})\n//\n//\t// Create a safe transformation function that copies the struct\n//\t// while excluding sensitive data\n//\tmakeEntrySafeFn := func(v any) any {\n//\t\tu := v.(*User)\n//\t\treturn \u0026User{\n//\t\t\tName:     u.Name,\n//\t\t\tBalance:  u.Balance,\n//\t\t\tInternal: \"\", // omit sensitive data\n//\t\t}\n//\t}\n//\n//\t// Create a read-only view of the tree\n//\tPublicTree := rotree.Wrap(tree, makeEntrySafeFn)\n//\n//\t// Safely access the data\n//\tvalue := roTree.Get(\"alice\")\n//\tuser := value.(*User)\n//\t// user.Name == \"Alice\"\n//\t// user.Balance == 100\n//\t// user.Internal == \"\" (sensitive data is filtered)\npackage rotree\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Wrap creates a new ReadOnlyTree from an existing avl.Tree and a safety transformation function.\n// If makeEntrySafeFn is nil, values will be returned as-is without transformation.\n//\n// makeEntrySafeFn is a function that transforms a tree entry into a safe version that can be exposed to external users.\n// This function should be implemented based on the specific safety requirements of your use case:\n//\n//  1. No-op transformation: For primitive types (int, string, etc.) or already safe objects,\n//     simply pass nil as the makeEntrySafeFn to return values as-is.\n//\n//  2. Defensive copying: For mutable types like slices or maps, you should create a deep copy\n//     to prevent modification of the original data.\n//     Example: func(v any) any { return append([]int{}, v.([]int)...) }\n//\n//  3. Read-only wrapper: Return a read-only version of the object that implements\n//     a limited interface.\n//     Example: func(v any) any { return NewReadOnlyObject(v) }\n//\n//  4. DAO transformation: Transform the object into a data access object that\n//     controls how the underlying data can be accessed.\n//     Example: func(v any) any { return NewDAO(v) }\n//\n// The function ensures that the returned object is safe to expose to untrusted code,\n// preventing unauthorized modifications to the original data structure.\nfunc Wrap(tree *avl.Tree, makeEntrySafeFn func(any) any) *ReadOnlyTree {\n\treturn \u0026ReadOnlyTree{\n\t\ttree:            tree,\n\t\tmakeEntrySafeFn: makeEntrySafeFn,\n\t}\n}\n\n// ReadOnlyTree wraps an avl.Tree and provides read-only access.\ntype ReadOnlyTree struct {\n\ttree            *avl.Tree\n\tmakeEntrySafeFn func(any) any\n}\n\n// IReadOnlyTree defines the read-only operations available on a tree.\ntype IReadOnlyTree interface {\n\tSize() int\n\tHas(key string) bool\n\tGet(key string) any\n\tGetByIndex(index int) (string, any)\n\tIterate(start, end string, cb avl.IterCbFn) bool\n\tReverseIterate(start, end string, cb avl.IterCbFn) bool\n\tIterateByOffset(offset int, count int, cb avl.IterCbFn) bool\n\tReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool\n}\n\n// Verify that ReadOnlyTree implements both ITree and IReadOnlyTree\nvar (\n\t_ avl.ITree     = (*ReadOnlyTree)(nil)\n\t_ IReadOnlyTree = (*ReadOnlyTree)(nil)\n)\n\n// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value\nfunc (roTree *ReadOnlyTree) getSafeValue(value any) any {\n\tif roTree.makeEntrySafeFn == nil {\n\t\treturn value\n\t}\n\treturn roTree.makeEntrySafeFn(value)\n}\n\n// Size returns the number of key-value pairs in the tree.\nfunc (roTree *ReadOnlyTree) Size() int {\n\treturn roTree.tree.Size()\n}\n\n// Has checks whether a key exists in the tree.\nfunc (roTree *ReadOnlyTree) Has(key string) bool {\n\treturn roTree.tree.Has(key)\n}\n\n// Get retrieves the value associated with the given key, converted to a safe format.\n// It returns the value if the key exists, or nil if it doesn't.\n// Note that a key stored with a nil value is indistinguishable\n// from an absent key; use Has to check for existence.\nfunc (roTree *ReadOnlyTree) Get(key string) any {\n\tvalue := roTree.tree.Get(key)\n\tif value == nil {\n\t\treturn nil\n\t}\n\treturn roTree.getSafeValue(value)\n}\n\n// GetByIndex retrieves the key-value pair at the specified index in the tree, with the value converted to a safe format.\nfunc (roTree *ReadOnlyTree) GetByIndex(index int) (string, any) {\n\tkey, value := roTree.tree.GetByIndex(index)\n\treturn key, roTree.getSafeValue(value)\n}\n\n// Iterate performs an in-order traversal of the tree within the specified key range.\nfunc (roTree *ReadOnlyTree) Iterate(start, end string, cb avl.IterCbFn) bool {\n\treturn roTree.tree.Iterate(start, end, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range.\nfunc (roTree *ReadOnlyTree) ReverseIterate(start, end string, cb avl.IterCbFn) bool {\n\treturn roTree.tree.ReverseIterate(start, end, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// IterateByOffset performs an in-order traversal of the tree starting from the specified offset.\nfunc (roTree *ReadOnlyTree) IterateByOffset(offset int, count int, cb avl.IterCbFn) bool {\n\treturn roTree.tree.IterateByOffset(offset, count, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset.\nfunc (roTree *ReadOnlyTree) ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool {\n\treturn roTree.tree.ReverseIterateByOffset(offset, count, func(key string, value any) bool {\n\t\treturn cb(key, roTree.getSafeValue(value))\n\t})\n}\n\n// Set is not supported on ReadOnlyTree and will panic.\nfunc (roTree *ReadOnlyTree) Set(key string, value any) bool {\n\tpanic(\"Set operation not supported on ReadOnlyTree\")\n}\n\n// Remove is not supported on ReadOnlyTree and will panic.\nfunc (roTree *ReadOnlyTree) Remove(key string) (value any, removed bool) {\n\tpanic(\"Remove operation not supported on ReadOnlyTree\")\n}\n\n// RemoveByIndex is not supported on ReadOnlyTree and will panic.\nfunc (roTree *ReadOnlyTree) RemoveByIndex(index int) (key string, value any) {\n\tpanic(\"RemoveByIndex operation not supported on ReadOnlyTree\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"pager","path":"gno.land/p/nt/avl/v0/pager","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/avl/v0/pager\"\ngno = \"0.9\"\n"},{"name":"pager.gno","body":"package pager\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Pager is a struct that holds the AVL tree and pagination parameters.\ntype Pager struct {\n\tTree            rotree.IReadOnlyTree\n\tPageQueryParam  string\n\tSizeQueryParam  string\n\tDefaultPageSize int\n\tReversed        bool\n}\n\n// Page represents a single page of results.\ntype Page struct {\n\tItems      []Item\n\tPageNumber int\n\tPageSize   int\n\tTotalItems int\n\tTotalPages int\n\tHasPrev    bool\n\tHasNext    bool\n\tPager      *Pager // Reference to the parent Pager\n}\n\n// Item represents a key-value pair in the AVL tree.\ntype Item struct {\n\tKey   string\n\tValue any\n}\n\n// NewPager creates a new Pager with default values.\nfunc NewPager(tree rotree.IReadOnlyTree, defaultPageSize int, reversed bool) *Pager {\n\treturn \u0026Pager{\n\t\tTree:            tree,\n\t\tPageQueryParam:  \"page\",\n\t\tSizeQueryParam:  \"size\",\n\t\tDefaultPageSize: defaultPageSize,\n\t\tReversed:        reversed,\n\t}\n}\n\n// GetPage retrieves a page of results from the AVL tree.\nfunc (p *Pager) GetPage(pageNumber int) *Page {\n\treturn p.GetPageWithSize(pageNumber, p.DefaultPageSize)\n}\n\nfunc (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {\n\tif pageSize \u003c= 0 {\n\t\tpanic(\"GetPageWithSize: invalid page size\")\n\t}\n\n\ttotalItems := p.Tree.Size()\n\ttotalPages := int(math.Ceil(float64(totalItems) / float64(pageSize)))\n\n\tpage := \u0026Page{\n\t\tTotalItems: totalItems,\n\t\tTotalPages: totalPages,\n\t\tPageSize:   pageSize,\n\t\tPager:      p,\n\t}\n\n\t// page number provided is not available\n\tif pageNumber \u003c 1 {\n\t\tpage.HasNext = totalPages \u003e 0\n\t\treturn page\n\t}\n\n\t// page number provided is outside the range of total pages\n\tif pageNumber \u003e totalPages {\n\t\tpage.PageNumber = pageNumber\n\t\tpage.HasPrev = pageNumber \u003e 0\n\t\treturn page\n\t}\n\n\tstartIndex := (pageNumber - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\tif endIndex \u003e totalItems {\n\t\tendIndex = totalItems\n\t}\n\n\titems := []Item{}\n\n\tif p.Reversed {\n\t\tp.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value any) bool {\n\t\t\titems = append(items, Item{Key: key, Value: value})\n\t\t\treturn false\n\t\t})\n\t} else {\n\t\tp.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value any) bool {\n\t\t\titems = append(items, Item{Key: key, Value: value})\n\t\t\treturn false\n\t\t})\n\t}\n\n\tpage.Items = items\n\tpage.PageNumber = pageNumber\n\tpage.HasPrev = pageNumber \u003e 1\n\tpage.HasNext = pageNumber \u003c totalPages\n\treturn page\n}\n\nfunc (p *Pager) MustGetPageByPath(rawURL string) *Page {\n\tpage, err := p.GetPageByPath(rawURL)\n\tif err != nil {\n\t\tpanic(\"invalid path\")\n\t}\n\treturn page\n}\n\n// GetPageByPath retrieves a page of results based on the query parameters in the URL path.\nfunc (p *Pager) GetPageByPath(rawURL string) (*Page, error) {\n\tpageNumber, pageSize, err := p.ParseQuery(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.GetPageWithSize(pageNumber, pageSize), nil\n}\n\n// Picker generates the Markdown UI for the page Picker\nfunc (p *Page) Picker(path string) string {\n\tpageNumber := p.PageNumber\n\tpageNumber = max(pageNumber, 1)\n\n\tif p.TotalPages \u003c= 1 {\n\t\treturn \"\"\n\t}\n\n\tu, _ := url.Parse(path)\n\tquery := u.Query()\n\n\t// Remove existing page query parameter\n\tquery.Del(p.Pager.PageQueryParam)\n\n\t// Encode remaining query parameters\n\tbaseQuery := query.Encode()\n\tif baseQuery != \"\" {\n\t\tbaseQuery = \"\u0026\" + baseQuery\n\t}\n\tmd := \"\"\n\n\tif p.HasPrev {\n\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d%s) | \", 1, p.Pager.PageQueryParam, 1, baseQuery)\n\n\t\tif p.PageNumber \u003e 4 {\n\t\t\tmd += \"… | \"\n\t\t}\n\n\t\tif p.PageNumber \u003e 3 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d%s) | \", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2, baseQuery)\n\t\t}\n\n\t\tif p.PageNumber \u003e 2 {\n\t\t\tmd += ufmt.Sprintf(\"[%d](?%s=%d%s) | \", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1, baseQuery)\n\t\t}\n\t}\n\n\tif p.PageNumber \u003e 0 \u0026\u0026 p.PageNumber \u003c= p.TotalPages {\n\t\tmd += ufmt.Sprintf(\"**%d**\", p.PageNumber)\n\t} else {\n\t\tmd += ufmt.Sprintf(\"_%d_\", p.PageNumber)\n\t}\n\n\tif p.HasNext {\n\t\tif p.PageNumber \u003c p.TotalPages-1 {\n\t\t\tmd += ufmt.Sprintf(\" | [%d](?%s=%d%s)\", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1, baseQuery)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-2 {\n\t\t\tmd += ufmt.Sprintf(\" | [%d](?%s=%d%s)\", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2, baseQuery)\n\t\t}\n\n\t\tif p.PageNumber \u003c p.TotalPages-3 {\n\t\t\tmd += \" | …\"\n\t\t}\n\n\t\tmd += ufmt.Sprintf(\" | [%d](?%s=%d%s)\", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages, baseQuery)\n\t}\n\n\treturn md\n}\n\n// ParseQuery parses the URL to extract the page number and page size.\nfunc (p *Pager) ParseQuery(rawURL string) (int, int, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn 1, p.DefaultPageSize, err\n\t}\n\n\tquery := u.Query()\n\tpageNumber := 1\n\tpageSize := p.DefaultPageSize\n\n\tif p.PageQueryParam != \"\" {\n\t\tif pageStr := query.Get(p.PageQueryParam); pageStr != \"\" {\n\t\t\tpageNumber, err = strconv.Atoi(pageStr)\n\t\t\tif err != nil || pageNumber \u003c 1 {\n\t\t\t\tpageNumber = 1\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.SizeQueryParam != \"\" {\n\t\tif sizeStr := query.Get(p.SizeQueryParam); sizeStr != \"\" {\n\t\t\tpageSize, err = strconv.Atoi(sizeStr)\n\t\t\tif err != nil || pageSize \u003c 1 {\n\t\t\t\tpageSize = p.DefaultPageSize\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pageNumber, pageSize, nil\n}\n\nfunc max(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"mux","path":"gno.land/p/nt/mux/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `mux` - Path router for Render\n\nSimple routing and rendering library for `Render(path)` requests in Gno realms. Similar in spirit to `http.ServeMux`, with support for path variables (`{name}`), wildcards (`*`), and query strings.\n\n## Usage\n\n```go\npackage myrealm\n\nimport \"gno.land/p/nt/mux/v0\"\n\nvar router *mux.Router\n\nfunc init() {\n    router = mux.NewRouter()\n\n    // Static route.\n    router.HandleFunc(\"\", func(res *mux.ResponseWriter, req *mux.Request) {\n        res.Write(\"# Home\\n\")\n    })\n\n    // Named parameter.\n    router.HandleFunc(\"hello/{name}\", func(res *mux.ResponseWriter, req *mux.Request) {\n        name := req.GetVar(\"name\")\n        res.Write(\"Hello, \" + name + \"!\")\n    })\n\n    // Query string.\n    router.HandleFunc(\"search\", func(res *mux.ResponseWriter, req *mux.Request) {\n        q := req.Query.Get(\"q\")\n        res.Write(\"Searching for: \" + q)\n    })\n\n    // Wildcard - matches the rest of the path.\n    router.HandleFunc(\"files/*\", func(res *mux.ResponseWriter, req *mux.Request) {\n        res.Write(\"File path: \" + req.GetVar(\"*\"))\n    })\n}\n\n// Realm entry point.\nfunc Render(path string) string {\n    return router.Render(path)\n}\n```\n\n## API\n\n```go\ntype Router struct {\n    NotFoundHandler NotFoundHandler\n    // unexported\n}\n\nfunc NewRouter() *Router\n\nfunc (r *Router) HandleFunc(pattern string, fn HandlerFunc)\nfunc (r *Router) HandleFuncRlm(pattern string, fn HandlerFuncRlm) // rlm-aware handler\nfunc (r *Router) HandleErrFunc(pattern string, fn ErrHandlerFunc)\nfunc (r *Router) SetNotFoundHandler(handler NotFoundHandler)\nfunc (r *Router) Render(reqPath string) string\nfunc (r *Router) RenderRlm(_ int, rlm realm, reqPath string) string // dispatches rlm-aware routes\n\ntype Request struct {\n    Path        string     // path without query string\n    RawPath     string     // path including \"?...\" query string\n    HandlerPath string     // pattern that matched this request\n    Query       url.Values // parsed query parameters\n}\n\nfunc (r *Request) GetVar(key string) string\n\ntype ResponseWriter struct{ /* unexported */ }\n\nfunc (rw *ResponseWriter) Write(data string)\nfunc (rw *ResponseWriter) Output() string\n\ntype Handler struct {\n    Pattern string\n    Fn      HandlerFunc    // set by HandleFunc\n    FnRlm   HandlerFuncRlm // set by HandleFuncRlm\n}\n\ntype HandlerFunc     func(*ResponseWriter, *Request)\ntype HandlerFuncRlm  func(_ int, rlm realm, res *ResponseWriter, req *Request)\ntype ErrHandlerFunc  func(*ResponseWriter, *Request) error\ntype NotFoundHandler func(*ResponseWriter, *Request)\n```\n\n## Route patterns\n\n- `users` - static, matches exactly `users`.\n- `users/{id}` - named parameter, extracted with `req.GetVar(\"id\")`.\n- `files/*` - wildcard, captures all remaining segments. Extract with `req.GetVar(\"*\")`.\n\nRoutes are matched in registration order; the first match wins. If no route matches, `NotFoundHandler` runs (default writes `\"404\"`).\n\n## Notes\n\n- `HandleErrFunc` wraps an error-returning handler: a non-nil error is written as `\"Error: \" + err.Error()` to the response.\n- Query strings are parsed off `reqPath` (`?foo=bar`); access via `req.Query` (a `net/url.Values`).\n- `req.RawPath` keeps the original path including the query string; `req.Path` strips it.\n- `req.GetVar(...)` and `req.Query.Get(...)` return attacker-controlled path/query input. Wrap it with `sanitize.InlineText` from [`gno.land/p/nt/markdown/sanitize/v0`](../../markdown/sanitize/v0) before writing it into the response, or user input can inject Markdown structure.\n- Register realm-aware handlers with `HandleFuncRlm` and dispatch them with `RenderRlm(0, cur, path)`. The plain `Render` path only invokes non-rlm `Fn` handlers.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package mux provides a simple routing and rendering library for handling dynamic path-based requests in Gno contracts.\n//\n// The `mux` package aims to offer similar functionality to `http.ServeMux` in Go, but for Gno's Render() requests.\n// It allows you to define routes with dynamic parts and associate them with corresponding handler functions for rendering outputs.\n//\n// Usage:\n// 1. Create a new Router instance using `NewRouter()` to handle routing and rendering logic.\n// 2. Register routes and their associated handler functions using the `Handle(route, handler)` method.\n// 3. Implement the rendering logic within the handler functions, utilizing the `Request` and `ResponseWriter` types.\n// 4. Use the `Render(path)` method to process a given path and execute the corresponding handler function to obtain the rendered output.\n//\n// Route Patterns:\n// Routes can include dynamic parts enclosed in braces, such as \"users/{id}\" or \"hello/{name}\". The `Request` object's `GetVar(key)`\n// method allows you to extract the value of a specific variable from the path based on routing rules.\n//\n// Example:\n//\n//\trouter := mux.NewRouter()\n//\n//\t// Define a route with a variable and associated handler function\n//\trouter.HandleFunc(\"hello/{name}\", func(res *mux.ResponseWriter, req *mux.Request) {\n//\t\tname := req.GetVar(\"name\")\n//\t\tif name != \"\" {\n//\t\t\tres.Write(\"Hello, \" + name + \"!\")\n//\t\t} else {\n//\t\t\tres.Write(\"Hello, world!\")\n//\t\t}\n//\t})\n//\n//\t// Render the output for the \"/hello/Alice\" path\n//\toutput := router.Render(\"hello/Alice\")\n//\t// Output: \"Hello, Alice!\"\n//\n// Note: The `mux` package provides a basic routing and rendering mechanism for simple use cases. For more advanced routing features,\n// consider using more specialized libraries or frameworks.\npackage mux\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/mux/v0\"\ngno = \"0.9\"\n"},{"name":"handler.gno","body":"package mux\n\n// Handler stores a route pattern with one of two handler shapes.\n// Fn (HandlerFunc, no rlm) is set by HandleFunc; FnRlm (HandlerFuncRlm,\n// rlm-aware non-crossing) is set by HandleFuncRlm. Exactly one is set\n// per route. RenderRlm dispatches FnRlm with the supplied rlm; Render\n// dispatches Fn and panics if the matched route was registered with\n// HandleFuncRlm (caller used the wrong dispatch method).\ntype Handler struct {\n\tPattern string\n\tFn      HandlerFunc\n\tFnRlm   HandlerFuncRlm\n}\n\ntype HandlerFunc func(*ResponseWriter, *Request)\n\n// HandlerFuncRlm is the rlm-aware handler shape — non-crossing\n// (`_ int, rlm realm` first params) so callers thread cur as data\n// for the handler to forward to downstream crossing functions.\ntype HandlerFuncRlm func(_ int, rlm realm, res *ResponseWriter, req *Request)\n\ntype ErrHandlerFunc func(*ResponseWriter, *Request) error\n\ntype NotFoundHandler func(*ResponseWriter, *Request)\n\n// TODO: AutomaticIndex\n"},{"name":"helpers.gno","body":"package mux\n\nfunc defaultNotFoundHandler(res *ResponseWriter, req *Request) {\n\tres.Write(\"404\")\n}\n"},{"name":"request.gno","body":"package mux\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\n// Request represents an incoming request.\ntype Request struct {\n\t// Path is request path name.\n\t//\n\t// Note: use RawPath to obtain a raw path with query string.\n\tPath string\n\n\t// RawPath contains a whole request path, including query string.\n\tRawPath string\n\n\t// HandlerPath is handler rule that matches a request.\n\tHandlerPath string\n\n\t// Query contains the parsed URL query parameters.\n\tQuery url.Values\n}\n\n// GetVar retrieves a variable from the path based on routing rules.\nfunc (r *Request) GetVar(key string) string {\n\thandlerParts := strings.Split(r.HandlerPath, \"/\")\n\treqParts := strings.Split(r.Path, \"/\")\n\treqIndex := 0\n\tfor handlerIndex := 0; handlerIndex \u003c len(handlerParts); handlerIndex++ {\n\t\thandlerPart := handlerParts[handlerIndex]\n\t\tswitch {\n\t\tcase handlerPart == \"*\":\n\t\t\t// If a wildcard \"*\" is found, consume all remaining segments\n\t\t\twildcardParts := reqParts[reqIndex:]\n\t\t\treqIndex = len(reqParts)                // Consume all remaining segments\n\t\t\treturn strings.Join(wildcardParts, \"/\") // Return all remaining segments as a string\n\t\tcase strings.HasPrefix(handlerPart, \"{\") \u0026\u0026 strings.HasSuffix(handlerPart, \"}\"):\n\t\t\t// If a variable of the form {param} is found we compare it with the key\n\t\t\tparameter := handlerPart[1 : len(handlerPart)-1]\n\t\t\tif parameter == key {\n\t\t\t\treturn reqParts[reqIndex]\n\t\t\t}\n\t\t\treqIndex++\n\t\tdefault:\n\t\t\tif reqIndex \u003e= len(reqParts) || handlerPart != reqParts[reqIndex] {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treqIndex++\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"},{"name":"response.gno","body":"package mux\n\nimport \"strings\"\n\n// ResponseWriter represents the response writer.\ntype ResponseWriter struct {\n\toutput strings.Builder\n}\n\n// Write appends data to the response output.\nfunc (rw *ResponseWriter) Write(data string) {\n\trw.output.WriteString(data)\n}\n\n// Output returns the final response output.\nfunc (rw *ResponseWriter) Output() string {\n\treturn rw.output.String()\n}\n\n// TODO: func (rw *ResponseWriter) Header()...\n"},{"name":"router.gno","body":"package mux\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\n// Router handles the routing and rendering logic.\ntype Router struct {\n\troutes          []Handler\n\tNotFoundHandler NotFoundHandler\n}\n\n// NewRouter creates a new Router instance.\nfunc NewRouter() *Router {\n\treturn \u0026Router{\n\t\troutes:          make([]Handler, 0),\n\t\tNotFoundHandler: defaultNotFoundHandler,\n\t}\n}\n\n// Render renders the output for the given path using the registered route handler.\nfunc (r *Router) Render(reqPath string) string {\n\tclearPath, rawQuery, _ := strings.Cut(reqPath, \"?\")\n\tquery, _ := url.ParseQuery(rawQuery)\n\treqParts := strings.Split(clearPath, \"/\")\n\n\tfor _, route := range r.routes {\n\t\tpatParts := strings.Split(route.Pattern, \"/\")\n\t\twildcard := false\n\t\tfor _, part := range patParts {\n\t\t\tif part == \"*\" {\n\t\t\t\twildcard = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !wildcard \u0026\u0026 len(patParts) != len(reqParts) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatch := true\n\t\tfor i := 0; i \u003c len(patParts); i++ {\n\t\t\tpatPart := patParts[i]\n\t\t\treqPart := reqParts[i]\n\n\t\t\tif patPart == \"*\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif strings.HasPrefix(patPart, \"{\") \u0026\u0026 strings.HasSuffix(patPart, \"}\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif patPart != reqPart {\n\t\t\t\tmatch = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif match {\n\t\t\treq := \u0026Request{\n\t\t\t\tPath:        clearPath,\n\t\t\t\tRawPath:     reqPath,\n\t\t\t\tHandlerPath: route.Pattern,\n\t\t\t\tQuery:       query,\n\t\t\t}\n\t\t\tres := \u0026ResponseWriter{}\n\t\t\tif route.Fn == nil {\n\t\t\t\tpanic(\"Router.Render: route \" + route.Pattern + \" was registered via HandleFuncRlm; use RenderRlm to dispatch\")\n\t\t\t}\n\t\t\troute.Fn(res, req)\n\t\t\treturn res.Output()\n\t\t}\n\t}\n\n\t// not found\n\treq := \u0026Request{Path: reqPath, Query: query}\n\tres := \u0026ResponseWriter{}\n\tr.NotFoundHandler(res, req)\n\treturn res.Output()\n}\n\n// RenderRlm is the rlm-aware counterpart of Render. Dispatches matched\n// routes registered via HandleFuncRlm with the supplied rlm; routes\n// registered via the legacy HandleFunc still work — rlm is ignored.\n// Use this when the router carries any rlm-aware handlers.\nfunc (r *Router) RenderRlm(_ int, rlm realm, reqPath string) string {\n\tclearPath, rawQuery, _ := strings.Cut(reqPath, \"?\")\n\tquery, _ := url.ParseQuery(rawQuery)\n\treqParts := strings.Split(clearPath, \"/\")\n\n\tfor _, route := range r.routes {\n\t\tpatParts := strings.Split(route.Pattern, \"/\")\n\t\twildcard := false\n\t\tfor _, part := range patParts {\n\t\t\tif part == \"*\" {\n\t\t\t\twildcard = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !wildcard \u0026\u0026 len(patParts) != len(reqParts) {\n\t\t\tcontinue\n\t\t}\n\n\t\tmatch := true\n\t\tfor i := 0; i \u003c len(patParts); i++ {\n\t\t\tpatPart := patParts[i]\n\t\t\treqPart := reqParts[i]\n\n\t\t\tif patPart == \"*\" {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif strings.HasPrefix(patPart, \"{\") \u0026\u0026 strings.HasSuffix(patPart, \"}\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif patPart != reqPart {\n\t\t\t\tmatch = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif match {\n\t\t\treq := \u0026Request{\n\t\t\t\tPath:        clearPath,\n\t\t\t\tRawPath:     reqPath,\n\t\t\t\tHandlerPath: route.Pattern,\n\t\t\t\tQuery:       query,\n\t\t\t}\n\t\t\tres := \u0026ResponseWriter{}\n\t\t\tif route.FnRlm != nil {\n\t\t\t\troute.FnRlm(0, rlm, res, req)\n\t\t\t} else {\n\t\t\t\troute.Fn(res, req)\n\t\t\t}\n\t\t\treturn res.Output()\n\t\t}\n\t}\n\n\t// not found\n\treq := \u0026Request{Path: reqPath, Query: query}\n\tres := \u0026ResponseWriter{}\n\tr.NotFoundHandler(res, req)\n\treturn res.Output()\n}\n\n// HandleFunc registers a route and its handler function.\nfunc (r *Router) HandleFunc(pattern string, fn HandlerFunc) {\n\troute := Handler{Pattern: pattern, Fn: fn}\n\tr.routes = append(r.routes, route)\n}\n\n// HandleFuncRlm registers a route with a rlm-aware handler. Dispatch\n// must use Router.RenderRlm — calling Router.Render on a route registered\n// via HandleFuncRlm panics (no rlm to supply).\nfunc (r *Router) HandleFuncRlm(pattern string, fn HandlerFuncRlm) {\n\troute := Handler{Pattern: pattern, FnRlm: fn}\n\tr.routes = append(r.routes, route)\n}\n\n// HandleErrFunc registers a route and its error handler function.\nfunc (r *Router) HandleErrFunc(pattern string, fn ErrHandlerFunc) {\n\t// Convert ErrHandlerFunc to regular HandlerFunc\n\thandler := func(res *ResponseWriter, req *Request) {\n\t\tif err := fn(res, req); err != nil {\n\t\t\tres.Write(\"Error: \" + err.Error())\n\t\t}\n\t}\n\n\tr.HandleFunc(pattern, handler)\n}\n\n// SetNotFoundHandler sets custom message for 404 defaultNotFoundHandler.\nfunc (r *Router) SetNotFoundHandler(handler NotFoundHandler) {\n\tr.NotFoundHandler = handler\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"fqname","path":"gno.land/p/nt/fqname/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `fqname` - Fully qualified identifiers\n\nParse, construct, and link fully qualified Gno identifiers of the form `\u003cpkgpath\u003e.\u003cname\u003e` (e.g. `gno.land/p/nt/avl/v0.Tree`).\n\n## Usage\n\n```go\nimport \"gno.land/p/nt/fqname/v0\"\n\n// Split a fully qualified name\npkgpath, name := fqname.Parse(\"gno.land/p/nt/avl/v0.Tree\")\n// pkgpath == \"gno.land/p/nt/avl/v0\", name == \"Tree\"\n\n// Rebuild one from its parts\nid := fqname.Construct(\"gno.land/r/demo/foo20\", \"Token\")\n// id == \"gno.land/r/demo/foo20.Token\"\n\n// Render as a Markdown link (gno.land paths become clickable)\nlink := fqname.RenderLink(\"gno.land/r/demo/foo20\", \"Token\")\n// link == \"[gno.land/r/demo/foo20](/r/demo/foo20).Token\"\n```\n\n## API\n\n```go\n// Parse splits a fully qualified identifier into (pkgpath, name).\n// If no name is present (no dot after the last slash), name is \"\".\nfunc Parse(fqname string) (pkgpath, name string)\n\n// Construct joins pkgpath and name with a dot. If name is empty, returns pkgpath.\nfunc Construct(pkgpath, name string) string\n\n// RenderLink formats a fully qualified identifier as Markdown.\n// Paths starting with \"gno.land\" are turned into a link to the package;\n// other paths are returned as plain text. The slug is dot-appended and\n// markdown-escaped.\nfunc RenderLink(pkgPath, slug string) string\n```\n\n## Notes\n\n- `Parse` treats everything after the dot following the last slash as the name, so nested selectors like `Pkg.Type.Method` round-trip as a single name.\n- `RenderLink` only links `gno.land`-rooted paths; foreign domains (e.g. `github.com/...`) are returned unmodified except for the dot-joined slug.\n- `RenderLink` markdown-escapes the `slug`, but NOT `pkgPath`: a `]` or `)` in an untrusted `pkgPath` breaks out of the link. Pass validated package paths, or sanitize with [`gno.land/p/nt/markdown/sanitize/v0`](../../markdown/sanitize/v0) before rendering untrusted input.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package fqname provides utilities for handling fully qualified identifiers\n// in Gno, typically a package path followed by a dot and a symbol name.\npackage fqname\n"},{"name":"fqname.gno","body":"// Package fqname provides utilities for handling fully qualified identifiers in\n// Gno. A fully qualified identifier typically includes a package path followed\n// by a dot (.) and then the name of a variable, function, type, or other\n// package-level declaration.\npackage fqname\n\nimport (\n\t\"strings\"\n)\n\n// Parse splits a fully qualified identifier into its package path and name\n// components. It handles cases with and without slashes in the package path.\n//\n//\tpkgpath, name := fqname.Parse(\"gno.land/p/nt/avl/v0.Tree\")\n//\tufmt.Sprintf(\"Package: %s, Name: %s\\n\", id.Package, id.Name)\n//\t// Output: Package: gno.land/p/nt/avl/v0, Name: Tree\nfunc Parse(fqname string) (pkgpath, name string) {\n\t// Find the index of the last slash.\n\tlastSlashIndex := strings.LastIndex(fqname, \"/\")\n\tif lastSlashIndex == -1 {\n\t\t// No slash found, handle it as a simple package name with dot notation.\n\t\tdotIndex := strings.LastIndex(fqname, \".\")\n\t\tif dotIndex == -1 {\n\t\t\treturn fqname, \"\"\n\t\t}\n\t\treturn fqname[:dotIndex], fqname[dotIndex+1:]\n\t}\n\n\t// Get the part after the last slash.\n\tafterSlash := fqname[lastSlashIndex+1:]\n\n\t// Check for a dot in the substring after the last slash.\n\tdotIndex := strings.Index(afterSlash, \".\")\n\tif dotIndex == -1 {\n\t\t// No dot found after the last slash\n\t\treturn fqname, \"\"\n\t}\n\n\t// Split at the dot to separate the base and the suffix.\n\tbase := fqname[:lastSlashIndex+1+dotIndex]\n\tsuffix := afterSlash[dotIndex+1:]\n\n\treturn base, suffix\n}\n\n// Construct a qualified identifier.\n//\n//\tfqName := fqname.Construct(\"gno.land/r/demo/foo20\", \"Token\")\n//\tfmt.Println(\"Fully Qualified Name:\", fqName)\n//\t// Output: gno.land/r/demo/foo20.Token\nfunc Construct(pkgpath, name string) string {\n\t// TODO: ensure pkgpath is valid - and as such last part does not contain a dot.\n\tif name == \"\" {\n\t\treturn pkgpath\n\t}\n\treturn pkgpath + \".\" + name\n}\n\n// RenderLink creates a formatted link for a fully qualified identifier.\n// If the package path starts with \"gno.land\", it converts it to a markdown link.\n// If the domain is different or missing, it returns the input as is.\nfunc RenderLink(pkgPath, slug string) string {\n\tif strings.HasPrefix(pkgPath, \"gno.land\") {\n\t\tpkgLink := strings.TrimPrefix(pkgPath, \"gno.land\")\n\t\tif slug != \"\" {\n\t\t\tsafeSlug := escapeMarkdown(slug)\n\t\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \").\" + safeSlug\n\t\t}\n\n\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \")\"\n\t}\n\n\tif slug != \"\" {\n\t\tsafeSlug := escapeMarkdown(slug)\n\t\treturn pkgPath + \".\" + safeSlug\n\t}\n\n\treturn pkgPath\n}\n\n// escapeMarkdown escapes characters that could break markdown link syntax.\nfunc escapeMarkdown(s string) string {\n\tr := strings.NewReplacer(\n\t\t\"[\", `\\[`,\n\t\t\"]\", `\\]`,\n\t\t\"(\", `\\(`,\n\t\t\")\", `\\)`,\n\t)\n\treturn r.Replace(s)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/fqname/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc20reg","path":"gno.land/r/demo/defi/grc20reg","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/grc20reg\"\ngno = \"0.9\"\n"},{"name":"grc20reg.gno","body":"package grc20reg\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar registry = avl.NewTree() // rlmPath.symbol -\u003e *Token\n\n// Construction lives in grc20.NewToken — it takes rlm realm last\n// and binds origRealm from rlm.PkgPath() under an IsCurrent assertion.\n// The registry key is the canonical fqname rlmPath.symbol (one token per\n// realm+symbol), independent of Token.ID()'s trailing sequence id, so\n// callers can look a token up from the (realm, symbol) pair they already\n// know:\n//\n//\tToken, ledger := grc20.NewToken(name, symbol, decimals, id, cur)\n//\tkey := grc20reg.Register(cross(cur), Token, \"\")\n\n// Register records token under its rlmPath.symbol key and returns that key.\n// Token.ID() carries a trailing sequence id (rlmPath.symbol.\u003cid\u003e) that keeps\n// token identities/events unique, but the registry deliberately keys by\n// rlmPath.symbol so lookups don't need to know the id, and so a realm cannot\n// register two tokens under the same symbol (overwrite/alias guard).\nfunc Register(cur realm, token *grc20.Token, slug string) string {\n\tif token == nil {\n\t\tpanic(\"grc20reg: nil token\")\n\t}\n\tif slug != \"\" {\n\t\tvalidateSlug(slug)\n\t}\n\trlmPath := cur.Previous().PkgPath()\n\tkey := fqname.Construct(rlmPath, token.GetSymbol())\n\t// Token.ID() == key + \".\" + \u003cid\u003e; verify the token originates from the\n\t// registering realm and symbol.\n\tif !strings.HasPrefix(token.ID(), key+\".\") {\n\t\tpanic(\"grc20reg: token must be registered from its own realm\")\n\t}\n\tif registry.Has(key) {\n\t\tpanic(\"grc20reg: token already registered\")\n\t}\n\tregistry.Set(key, token)\n\tchain.Emit(\n\t\tregisterEvent,\n\t\t\"token_path\", key,\n\t\t\"pkgpath\", rlmPath,\n\t\t\"slug\", slug,\n\t\t\"symbol\", token.GetSymbol(),\n\t)\n\treturn key\n}\n\nfunc Get(key string) *grc20.Token {\n\ttoken := registry.Get(key)\n\tif token == nil {\n\t\treturn nil\n\t}\n\treturn token.(*grc20.Token)\n}\n\nfunc MustGet(key string) *grc20.Token {\n\ttoken := Get(key)\n\tif token == nil {\n\t\tpanic(\"unknown token: \" + key)\n\t}\n\treturn token\n}\n\n// Transfer moves tokens owned by the immediate caller. A direct user call\n// spends the user's balance. A realm cross-call spends the calling realm's\n// balance.\n//\n// This differs from calling `Get(tokenKey).CallerTeller().Transfer` within that\n// realm, which spends the balance of the realm's previous caller. To act as\n// your own realm explicitly, use `Get(tokenKey).RealmTeller(0, cur)`.\nfunc Transfer(cur realm, tokenKey string, to address, amount int64) {\n\tcheckErr(MustGet(tokenKey).CallerTeller().Transfer(0, cur, to, amount))\n}\n\n// Approve sets an allowance owned by the immediate caller. A direct user call\n// updates the user's allowance. A realm cross-call updates the calling realm's\n// allowance.\n//\n// This differs from calling `Get(tokenKey).CallerTeller().Approve` within that\n// realm, which updates the allowance of the realm's previous caller. To act as\n// your own realm explicitly, use `Get(tokenKey).RealmTeller(0, cur)`.\nfunc Approve(cur realm, tokenKey string, spender address, amount int64) {\n\tcheckErr(MustGet(tokenKey).CallerTeller().Approve(0, cur, spender, amount))\n}\n\n// TransferFrom spends an allowance as the immediate caller. A direct user call\n// uses the user as the spender. A realm cross-call uses the calling realm as\n// the spender.\n//\n// This differs from calling `Get(tokenKey).CallerTeller().TransferFrom` within\n// that realm, which uses the realm's previous caller as the spender. To act as\n// your own realm explicitly, use `Get(tokenKey).RealmTeller(0, cur)`.\nfunc TransferFrom(cur realm, tokenKey string, from, to address, amount int64) {\n\tcheckErr(MustGet(tokenKey).CallerTeller().TransferFrom(0, cur, from, to, amount))\n}\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\": // home\n\t\t// TODO: add pagination\n\t\ts := \"\"\n\t\tcount := 0\n\t\tregistry.Iterate(\"\", \"\", func(key string, tokenI any) bool {\n\t\t\tcount++\n\t\t\ttoken := tokenI.(*grc20.Token)\n\t\t\trlmPath, tokenID := fqname.Parse(key)\n\t\t\trlmLink := fqname.RenderLink(rlmPath, tokenID)\n\t\t\tinfoLink := \"/r/demo/grc20reg:\" + key\n\t\t\ts += \"- \" + md.Bold(md.EscapeText(token.GetName())) + \" - \" + rlmLink + \" - \" + md.Link(\"info\", infoLink) + \"\\n\"\n\t\t\treturn false\n\t\t})\n\t\tif count == 0 {\n\t\t\treturn \"No registered token.\"\n\t\t}\n\t\treturn s\n\tdefault: // specific token\n\t\tkey := path\n\t\ttoken := MustGet(key)\n\t\trlmPath, tokenID := fqname.Parse(key)\n\t\trlmLink := fqname.RenderLink(rlmPath, tokenID)\n\t\ts := ufmt.Sprintf(\"# %s\\n\", md.EscapeText(token.GetName()))\n\t\ts += \"- symbol: \" + md.Bold(md.EscapeText(token.GetSymbol())) + \"\\n\"\n\t\ts += ufmt.Sprintf(\"- realm: %s\\n\", rlmLink)\n\t\ts += ufmt.Sprintf(\"- decimals: %d\\n\", token.GetDecimals())\n\t\ts += ufmt.Sprintf(\"- total supply: %d\\n\", token.TotalSupply())\n\t\treturn s\n\t}\n}\n\nconst (\n\tregisterEvent = \"register\"\n\tmaxSlugLen    = 128\n)\n\nfunc GetRegistry() *rotree.ReadOnlyTree {\n\treturn rotree.Wrap(registry, nil)\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// validateSlug panics if the slug is too long or contains non-alphanumeric characters.\n// Only letters, digits, dashes, and underscores are allowed.\nfunc validateSlug(slug string) {\n\tif len(slug) \u003e maxSlugLen {\n\t\tpanic(\"grc20reg: slug too long\")\n\t}\n\tfor _, c := range slug {\n\t\tif !isAlphanumeric(c) \u0026\u0026 c != '_' \u0026\u0026 c != '-' {\n\t\t\tpanic(\"grc20reg: invalid slug character: \" + string(c))\n\t\t}\n\t}\n}\n\nfunc isAlphanumeric(c rune) bool {\n\treturn (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') || (c \u003e= '0' \u0026\u0026 c \u003c= '9')\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc20factory","path":"gno.land/r/demo/defi/grc20factory","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/grc20factory\"\ngno = \"0.9\"\n"},{"name":"grc20factory.gno","body":"package grc20factory\n\nimport (\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/avl/v0\"\n\tp \"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nvar (\n\tinstances   avl.Tree // symbol -\u003e *instance\n\tnextTokenID seqid.ID\n\tpager       = p.NewPager(rotree.Wrap(\u0026instances, nil), 20, false)\n)\n\ntype instance struct {\n\ttoken  *grc20.Token\n\tledger *grc20.PrivateLedger\n\tadmin  *ownable.Ownable\n\tfaucet int64 // per-request amount. disabled if 0.\n}\n\nfunc New(cur realm, name, symbol string, decimals int, initialMint, faucet int64) {\n\tcaller := cur.Previous().Address()\n\tNewWithAdmin(cur, name, symbol, decimals, initialMint, faucet, caller)\n}\n\nfunc NewWithAdmin(cur realm, name, symbol string, decimals int, initialMint, faucet int64, admin address) {\n\texists := instances.Has(symbol)\n\tif exists {\n\t\tpanic(\"token already exists\")\n\t}\n\n\ttoken, ledger := grc20.NewToken(name, symbol, decimals, nextTokenID.Next(), cur)\n\tif initialMint \u003e 0 {\n\t\tledger.Mint(admin, initialMint)\n\t}\n\n\tinst := instance{\n\t\ttoken:  token,\n\t\tledger: ledger,\n\t\tadmin:  ownable.NewWithAddress(admin),\n\t\tfaucet: faucet,\n\t}\n\tinstances.Set(symbol, \u0026inst)\n\n\tgrc20reg.Register(cross(cur), token, symbol)\n}\n\nfunc Bank(symbol string) *grc20.Token {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token\n}\n\nfunc TotalSupply(symbol string) int64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().TotalSupply()\n}\n\nfunc HasAddr(symbol string, owner address) bool {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.HasAddr(owner)\n}\n\nfunc BalanceOf(symbol string, owner address) int64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().BalanceOf(owner)\n}\n\nfunc Allowance(symbol string, owner, spender address) int64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, symbol string, to address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := cur.Previous().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.Transfer(0, cur, to, amount))\n}\n\nfunc Approve(cur realm, symbol string, spender address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := cur.Previous().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.Approve(0, cur, spender, amount))\n}\n\nfunc TransferFrom(cur realm, symbol string, from, to address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := cur.Previous().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.TransferFrom(0, cur, from, to, amount))\n}\n\n// faucet.\nfunc Faucet(cur realm, symbol string) {\n\tinst := mustGetInstance(symbol)\n\tif inst.faucet == 0 {\n\t\tpanic(\"faucet disabled for this token\")\n\t}\n\t// FIXME: add limits?\n\t// FIXME: add payment in gnot?\n\tcaller := cur.Previous().Address()\n\tcheckErr(inst.ledger.Mint(caller, inst.faucet))\n}\n\nfunc Mint(cur realm, symbol string, to address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertOwnedBy(cur.Previous().Address())\n\tcheckErr(inst.ledger.Mint(to, amount))\n}\n\nfunc Burn(cur realm, symbol string, from address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertOwnedBy(cur.Previous().Address())\n\tcheckErr(inst.ledger.Burn(from, amount))\n}\n\n// instance admin functionality\nfunc DropInstanceOwnership(cur realm, symbol string) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.admin.DropOwnership(0, cur))\n}\n\nfunc TransferInstanceOwnership(cur realm, symbol string, newOwner address) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.admin.TransferOwnership(0, cur, newOwner))\n}\n\nfunc ListTokens(pageNumber, pageSize int) []*grc20.Token {\n\tpage := pager.GetPageWithSize(pageNumber, pageSize)\n\n\ttokens := make([]*grc20.Token, len(page.Items))\n\tfor i := range page.Items {\n\t\ttokens[i] = page.Items[i].Value.(*instance).token\n\t}\n\n\treturn tokens\n}\n\nfunc Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHome)\n\trouter.HandleFunc(\"{symbol}\", renderToken)\n\trouter.HandleFunc(\"{symbol}/balance/{address}\", renderBalance)\n\treturn router.Render(path)\n}\n\nfunc renderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tout := md.H1(ufmt.Sprintf(\"GRC20 Tokens (%d)\", instances.Size()))\n\n\t// Get the current page of tokens based on the request path.\n\tpage := pager.MustGetPageByPath(req.RawPath)\n\n\t// Render the list of tokens.\n\tfor _, item := range page.Items {\n\t\ttoken := item.Value.(*instance).token\n\t\tout += md.BulletItem(\n\t\t\tmd.Link(\n\t\t\t\tufmt.Sprintf(\"%s ($%s)\", token.GetName(), token.GetSymbol()),\n\t\t\t\tufmt.Sprintf(\"/r/demo/grc20factory:%s\", token.GetSymbol()),\n\t\t\t),\n\t\t)\n\t}\n\tout += \"\\n\"\n\n\t// Add the page picker.\n\tout += md.Paragraph(page.Picker(req.Path))\n\n\tres.Write(out)\n}\n\nfunc renderToken(res *mux.ResponseWriter, req *mux.Request) {\n\t// Get the token symbol from the request.\n\tsymbol := req.GetVar(\"symbol\")\n\tinst := mustGetInstance(symbol)\n\n\t// Render the token details.\n\tout := inst.token.RenderHome()\n\tout += md.BulletItem(\n\t\tufmt.Sprintf(\"%s: %s\", md.Bold(\"Admin\"), inst.admin.Owner()),\n\t)\n\n\tres.Write(out)\n}\n\nfunc renderBalance(res *mux.ResponseWriter, req *mux.Request) {\n\tvar (\n\t\tsymbol = req.GetVar(\"symbol\")\n\t\taddr   = req.GetVar(\"address\")\n\t)\n\n\t// Get the balance of the specified address for the token.\n\tinst := mustGetInstance(symbol)\n\tbalance := inst.token.CallerTeller().BalanceOf(address(addr))\n\n\t// Render the balance information.\n\tout := md.Paragraph(\n\t\tufmt.Sprintf(\"%s balance: %d\", md.Bold(addr), balance),\n\t)\n\n\tres.Write(out)\n}\n\nfunc mustGetInstance(symbol string) *instance {\n\tt := instances.Get(symbol)\n\tif t == nil {\n\t\tpanic(\"token instance does not exist\")\n\t}\n\treturn t.(*instance)\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"panictoerr","path":"gno.land/p/aeddi/panictoerr","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/aeddi/panictoerr\"\ngno = \"0.9\"\n"},{"name":"panictoerr.gno","body":"package panictoerr\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// PanicToError executes a function that might panic and, if it does,\n// recovers the panic and converts it to an error.\nfunc PanicToError(mightPanic func()) (err error) {\n\t// Catch any panic that might occur and convert it to an error.\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = anyToError(r)\n\t\t}\n\t}()\n\n\t// Execute the function that might panic.\n\tmightPanic()\n\n\treturn nil\n}\n\n// AbortToError executes a function that might abort, and if it does,\n// revives the abort and converts it to an error.\nfunc AbortToError(mightAbort func()) error {\n\t// Catch any abort that might occur and convert it to an error.\n\tif r := revive(mightAbort); r != nil {\n\t\treturn anyToError(r)\n\t}\n\n\treturn nil\n}\n\n// PanicAbortToError executes a function that might either panic or abort,\n// and if it does, it recovers the panic or revives the abort and converts\n// it to an error.\nfunc PanicAbortToError(mightPanicOrAbort func()) error {\n\tvar panicErr error\n\n\t// Catch any panic or abort that might occur and convert it to an error.\n\tif abortErr := AbortToError(func() {\n\t\tpanicErr = PanicToError(mightPanicOrAbort)\n\t}); abortErr != nil {\n\t\treturn abortErr\n\t}\n\n\treturn panicErr\n}\n\n// anyToError converts any value to an error.\nfunc anyToError(v any) error {\n\tswitch v := v.(type) {\n\tcase string:\n\t\treturn errors.New(v)\n\tcase error:\n\t\treturn v\n\tdefault:\n\t\treturn errors.New(ufmt.Sprint(v))\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"helplink","path":"gno.land/p/moul/helplink","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/helplink\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"helplink.gno","body":"// Package helplink provides utilities for creating help page links compatible\n// with Gnoweb, Gnobro, and other clients that support the Gno contracts'\n// flavored Markdown format.\n//\n// This package simplifies the generation of dynamic, context-sensitive help\n// links, enabling users to navigate relevant documentation seamlessly within\n// the Gno ecosystem.\n//\n// For a more lightweight alternative, consider using p/moul/txlink.\n//\n// The primary functions — Func, FuncURL, and Home — are intended for use with\n// the \"relative realm\". When specifying a custom Realm, you can create links\n// that utilize either the current realm path or a fully qualified path to\n// another realm.\npackage helplink\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar chainDomain = runtime.ChainDomain()\n\n// Func returns a markdown link for the specific function with optional\n// key-value arguments, for the current realm.\nfunc Func(title string, fn string, args ...string) string {\n\treturn Realm(\"\").Func(title, fn, args...)\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc FuncURL(fn string, args ...string) string {\n\treturn Realm(\"\").FuncURL(fn, args...)\n}\n\n// Home returns the URL for the help homepage of the current realm.\nfunc Home() string {\n\treturn Realm(\"\").Home()\n}\n\n// Realm represents a specific realm for generating help links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\tcurPath := unsafe.CurrentRealm().PkgPath()\n\t\treturn strings.TrimPrefix(curPath, chainDomain)\n\t}\n\n\t// local realm -\u003e /realm\n\trlmstr := string(r)\n\tif strings.HasPrefix(rlmstr, chainDomain) {\n\t\treturn strings.TrimPrefix(rlmstr, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + rlmstr\n}\n\n// Func returns a markdown link for the specified function with optional\n// key-value arguments.\nfunc (r Realm) Func(title string, fn string, args ...string) string {\n\t// XXX: escape title\n\treturn \"[\" + title + \"](\" + r.FuncURL(fn, args...) + \")\"\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments.\nfunc (r Realm) FuncURL(fn string, args ...string) string {\n\ttlr := txlink.Realm(r)\n\treturn tlr.Call(fn, args...)\n}\n\n// Home returns the base help URL for the specified realm.\nfunc (r Realm) Home() string {\n\treturn r.prefix() + \"$help\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"mdtable","path":"gno.land/p/moul/mdtable","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/mdtable\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"mdtable.gno","body":"// Package mdtable provides a simple way to create Markdown tables.\n//\n// Example usage:\n//\n//\timport \"gno.land/p/moul/mdtable\"\n//\n//\tfunc Render(path string) string {\n//\t    table := mdtable.Table{\n//\t        Headers: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n//\t    }\n//\t    table.Append([]string{\"#1\", \"Add a new validator\", \"succeed\", \"2024-01-01\"})\n//\t    table.Append([]string{\"#2\", \"Change parameter\", \"timed out\", \"2024-01-02\"})\n//\t    return table.String()\n//\t}\n//\n// Output:\n//\n//\t| ID | Title | Status | Date |\n//\t| --- | --- | --- | --- |\n//\t| #1 | Add a new validator | succeed | 2024-01-01 |\n//\t| #2 | Change parameter | timed out | 2024-01-02 |\npackage mdtable\n\nimport (\n\t\"strings\"\n)\n\ntype Table struct {\n\tHeaders []string\n\tRows    [][]string\n\t// XXX: optional headers alignment.\n}\n\nfunc (t *Table) Append(row []string) {\n\tt.Rows = append(t.Rows, row)\n}\n\nfunc (t Table) String() string {\n\t// XXX: switch to using text/tabwriter when porting to Gno to support\n\t// better-formatted raw Markdown output.\n\n\tif len(t.Headers) == 0 \u0026\u0026 len(t.Rows) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tif len(t.Headers) == 0 {\n\t\tt.Headers = make([]string, len(t.Rows[0]))\n\t}\n\n\t// Print header.\n\tsb.WriteString(\"| \" + strings.Join(t.Headers, \" | \") + \" |\\n\")\n\tsb.WriteString(\"|\" + strings.Repeat(\" --- |\", len(t.Headers)) + \"\\n\")\n\n\t// Print rows.\n\tfor _, row := range t.Rows {\n\t\tescapedRow := make([]string, len(row))\n\t\tfor i, cell := range row {\n\t\t\tescapedRow[i] = strings.ReplaceAll(cell, \"|\", \"\u0026#124;\") // Escape pipe characters.\n\t\t}\n\t\tsb.WriteString(\"| \" + strings.Join(escapedRow, \" | \") + \" |\\n\")\n\t}\n\n\treturn sb.String()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"list","path":"gno.land/p/nt/bptree/v0/list","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bptree/v0/list\"\ngno = \"0.9\"\n"},{"name":"list.gno","body":"// Package list implements a dynamic list data structure backed by a B+ tree.\n// It provides O(log n) operations for most list operations while maintaining\n// order stability.\n//\n// The list supports various operations including append, get, set, delete,\n// range queries, and iteration. It can store values of any type.\n//\n// Example usage:\n//\n//\t// Create a new list and add elements\n//\tvar l list.List\n//\tl.Append(1, 2, 3)\n//\n//\t// Get and set elements\n//\tvalue, _ := l.Get(1)  // returns 2\n//\tl.Set(1, 42)      // updates index 1 to 42\n//\n//\t// Delete elements\n//\tl.Delete(0)       // removes first element\n//\n//\t// Iterate over elements\n//\tl.ForEach(func(index int, value any) bool {\n//\t    ufmt.Printf(\"index %d: %v\\n\", index, value)\n//\t    return false  // continue iteration\n//\t})\n//\t// Output:\n//\t// index 0: 42\n//\t// index 1: 3\n//\n//\t// Create a list using a variable declaration\n//\tvar l2 list.List\n//\tl2.Append(4, 5, 6)\n//\tprintln(l2.Len())  // Output: 3\npackage list\n\nimport (\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/bptree/v0/rotree\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// IList defines the interface for list operations\ntype IList interface {\n\tLen() int\n\tAppend(values ...any)\n\tGet(index int) (any, bool)\n\tSet(index int, value any) bool\n\tDelete(index int) (any, bool)\n\tSlice(startIndex, endIndex int) []any\n\tForEach(fn func(index int, value any) bool)\n\tClone() *List\n\tDeleteRange(startIndex, endIndex int) int\n}\n\n// Verify List implements IList interface\nvar _ IList = (*List)(nil)\n\n// List represents an ordered sequence of items backed by a B+ tree\ntype List struct {\n\ttree  bptree.BPTree\n\tidGen seqid.ID\n}\n\n// Len returns the number of elements in the list.\nfunc (l *List) Len() int {\n\treturn l.tree.Size()\n}\n\n// Append adds one or more values to the end of the list.\nfunc (l *List) Append(values ...any) {\n\tfor _, v := range values {\n\t\tl.tree.Set(l.idGen.Next().String(), v)\n\t}\n}\n\n// Get returns the value at the specified index and true if the index is valid.\n// Returns (nil, false) if index is out of bounds.\nfunc (l *List) Get(index int) (any, bool) {\n\tif index \u003c 0 || index \u003e= l.tree.Size() {\n\t\treturn nil, false\n\t}\n\t_, value := l.tree.GetByIndex(index)\n\treturn value, true\n}\n\n// Set updates or appends a value at the specified index.\n// Returns true if the operation was successful, false otherwise.\n// For empty lists, only index 0 is valid (append case).\nfunc (l *List) Set(index int, value any) bool {\n\tsize := l.tree.Size()\n\n\t// Handle empty list case - only allow index 0\n\tif size == 0 {\n\t\tif index == 0 {\n\t\t\tl.Append(value)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tif index \u003c 0 || index \u003e size {\n\t\treturn false\n\t}\n\n\t// If setting at the end (append case)\n\tif index == size {\n\t\tl.Append(value)\n\t\treturn true\n\t}\n\n\t// Get the key at the specified index\n\tkey, _ := l.tree.GetByIndex(index)\n\tif key == \"\" {\n\t\treturn false\n\t}\n\n\t// Update the value at the existing key\n\tl.tree.Set(key, value)\n\treturn true\n}\n\n// Delete removes the element at the specified index.\n// Returns the deleted value and true if successful, nil and false otherwise.\nfunc (l *List) Delete(index int) (any, bool) {\n\tsize := l.tree.Size()\n\t// Always return nil, false for empty list\n\tif size == 0 {\n\t\treturn nil, false\n\t}\n\n\tif index \u003c 0 || index \u003e= size {\n\t\treturn nil, false\n\t}\n\n\tkey, value := l.tree.GetByIndex(index)\n\tif key == \"\" {\n\t\treturn nil, false\n\t}\n\n\tl.tree.Remove(key)\n\treturn value, true\n}\n\n// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive).\n// Returns nil if the range is invalid.\nfunc (l *List) Slice(startIndex, endIndex int) []any {\n\tsize := l.tree.Size()\n\n\t// Normalize bounds\n\tif startIndex \u003c 0 {\n\t\tstartIndex = 0\n\t}\n\tif endIndex \u003e size {\n\t\tendIndex = size\n\t}\n\tif startIndex \u003e= endIndex {\n\t\treturn nil\n\t}\n\n\tcount := endIndex - startIndex\n\tresult := make([]any, count)\n\n\ti := 0\n\tl.tree.IterateByOffset(startIndex, count, func(_ string, value any) bool {\n\t\tresult[i] = value\n\t\ti++\n\t\treturn false\n\t})\n\treturn result\n}\n\n// ForEach iterates through all elements in the list.\nfunc (l *List) ForEach(fn func(index int, value any) bool) {\n\tif l.tree.Size() == 0 {\n\t\treturn\n\t}\n\n\tindex := 0\n\tl.tree.IterateByOffset(0, l.tree.Size(), func(_ string, value any) bool {\n\t\tresult := fn(index, value)\n\t\tindex++\n\t\treturn result\n\t})\n}\n\n// Clone creates a shallow copy of the list.\nfunc (l *List) Clone() *List {\n\tnewList := \u0026List{\n\t\ttree:  bptree.BPTree{},\n\t\tidGen: l.idGen,\n\t}\n\n\tsize := l.tree.Size()\n\tif size == 0 {\n\t\treturn newList\n\t}\n\n\tl.tree.IterateByOffset(0, size, func(_ string, value any) bool {\n\t\tnewList.Append(value)\n\t\treturn false\n\t})\n\n\treturn newList\n}\n\n// DeleteRange removes elements from startIndex (inclusive) to endIndex (exclusive).\n// Returns the number of elements deleted.\nfunc (l *List) DeleteRange(startIndex, endIndex int) int {\n\tsize := l.tree.Size()\n\n\t// Normalize bounds\n\tif startIndex \u003c 0 {\n\t\tstartIndex = 0\n\t}\n\tif endIndex \u003e size {\n\t\tendIndex = size\n\t}\n\tif startIndex \u003e= endIndex {\n\t\treturn 0\n\t}\n\n\t// Collect keys to delete\n\tkeysToDelete := make([]string, 0, endIndex-startIndex)\n\tl.tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, _ any) bool {\n\t\tkeysToDelete = append(keysToDelete, key)\n\t\treturn false\n\t})\n\n\t// Delete collected keys\n\tfor _, key := range keysToDelete {\n\t\tl.tree.Remove(key)\n\t}\n\n\treturn len(keysToDelete)\n}\n\n// Tree returns a read-only pointer to the underlying B+ tree.\nfunc (l *List) Tree() *rotree.ReadOnlyTree {\n\treturn rotree.Wrap(\u0026l.tree, nil)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"treasury","path":"gno.land/p/nt/treasury/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `treasury` - Coin and GRC20 treasury management\n\nTreasury management for coin and GRC20 token transfers in Gno realms. A `Treasury` holds a set of `Banker`s, each responsible for sending a specific asset type, and records the payment history per banker.\n\n# 1. Concepts\n\n- **Treasury**: container that registers one or more `Banker`s and exposes a unified `Send`/`History`/`Balances` API. Also provides a `Render` router for gnoweb pages.\n- **Banker**: handler for a single asset type. Built-ins are `CoinsBanker` (native chain coins) and `GRC20Banker` (any number of GRC20 tokens, resolved through a user-supplied `TokenListerFunc`).\n- **Payment**: opaque value produced by a banker-specific helper (`NewCoinsPayment`, `NewGRC20Payment`). Each `Payment` is bound to a `BankerID()`, which is how the treasury routes it.\n\n# 2. Usage\n\n```go\nimport (\n    \"chain\"\n    \"chain/banker\"\n    \"chain/runtime\"\n\n    \"gno.land/p/demo/tokens/grc20\"\n    \"gno.land/p/nt/treasury/v0\"\n)\n\nvar (\n    tokens = map[string]*grc20.Token{}\n    tr     *treasury.Treasury\n)\n\nfunc init() {\n    owner := runtime.CurrentRealm().Address() // this realm holds and sends the funds\n\n    // Coins banker owned by this realm.\n    coinsBanker, err := treasury.NewCoinsBankerWithOwner(\n        owner,\n        banker.NewBanker(banker.BankerTypeRealmSend),\n    )\n    if err != nil {\n        panic(err)\n    }\n\n    // GRC20 banker that resolves tokens through a lister.\n    grc20Banker, err := treasury.NewGRC20BankerWithOwner(owner, func() map[string]*grc20.Token {\n        return tokens\n    })\n    if err != nil {\n        panic(err)\n    }\n\n    tr, err = treasury.New(\n        []treasury.Banker{coinsBanker, grc20Banker},\n        runtime.CurrentRealm().PkgPath(),\n    )\n    if err != nil {\n        panic(err)\n    }\n}\n\n// SendUgnot transfers ugnot from the realm to `to`.\nfunc SendUgnot(cur realm, to address, amount int64) {\n    p := treasury.NewCoinsPayment(chain.Coins{{Denom: \"ugnot\", Amount: amount}}, to)\n    if err := tr.Send(0, cur, p); err != nil {\n        panic(err)\n    }\n}\n\n// Render exposes the treasury under the realm's render path.\nfunc Render(path string) string {\n    return tr.Render(path)\n}\n```\n\n# 3. API\n\n## 3.1 Treasury\n\n```go\n// Builds a treasury with the provided bankers (at least one required, IDs must be unique).\n// pkgPath is the realm's package path, used as the base for the Render router.\nfunc New(bankers []Banker, pkgPath string) (*Treasury, error)\n\nfunc (t *Treasury) Send(_ int, rlm realm, p Payment) error\nfunc (t *Treasury) History(bankerID string, pageNumber, pageSize int) ([]Payment, error)\nfunc (t *Treasury) Balances(bankerID string) ([]Balance, error)\nfunc (t *Treasury) Address(bankerID string) (string, error)\nfunc (t *Treasury) HasBanker(bankerID string) bool\nfunc (t *Treasury) ListBankerIDs() []string\n\n// Render entry points (a mux router is initialized by `New`).\nfunc (t *Treasury) Render(path string) string\nfunc (t *Treasury) RenderLanding(path string) string\nfunc (t *Treasury) RenderBanker(bankerID, path string) string\nfunc (t *Treasury) RenderBankerHistory(bankerID, path string) string\n```\n\nRender routes:\n- `\"\"` — landing page, lists each banker.\n- `{banker}` — banker details (address, balances, last N payments).\n- `{banker}/history` — paginated payment history.\n\nThe `history_size` query parameter on `{banker}` controls the preview size (default `5`, `0` hides the preview).\n\n## 3.2 Banker and Payment interfaces\n\n```go\ntype Banker interface {\n    ID() string                     // unique banker ID used for routing\n    Send(int, realm, Payment) error // thread the caller's cur; pass 0 as the first arg\n    Balances() []Balance\n    Address() string                // address used to receive payments\n}\n\ntype Payment interface {\n    BankerID() string    // routes the payment to a banker\n    String() string\n}\n\ntype Balance struct {\n    Denom  string\n    Amount int64\n}\n\n// Capability guard: any entry point that accepts a Banker from an external\n// caller MUST verify it before invoking its methods. Validates dynamic type\n// only (embedding-based wrappers are rejected), not captured state.\nfunc IsCanonicalBanker(b Banker) bool\n```\n\n## 3.3 CoinsBanker\n\n`Banker` for native chain coins. Owns an address and an inner `chain/banker.Banker` (must be the canonical one returned by `banker.NewBanker` — fake implementations are rejected).\n\n```go\nfunc NewCoinsBankerWithOwner(owner address, banker_ banker.Banker) (*CoinsBanker, error)\n\nfunc NewCoinsPayment(coins chain.Coins, toAddress address) Payment\n```\n\n`CoinsBanker.ID()` returns `\"Coins\"`.\n\n## 3.4 GRC20Banker\n\n`Banker` for GRC20 tokens. Tokens are resolved at send time through a `TokenListerFunc`, so the set of supported tokens can change without rebuilding the banker.\n\n```go\ntype TokenListerFunc func() map[string]*grc20.Token\n\nfunc NewGRC20BankerWithOwner(owner address, lister TokenListerFunc) (*GRC20Banker, error)\n\nfunc NewGRC20Payment(tokenKey string, amount int64, toAddress address) Payment\n```\n\n`GRC20Banker.ID()` returns `\"GRC20\"`. `tokenKey` must be a key in the map returned by the lister.\n\n## 3.5 Errors\n\n```go\nErrNoBankerProvided       // New called with empty bankers slice\nErrDuplicateBanker        // two bankers share the same ID\nErrBankerNotFound         // Send/History/... called with an unknown banker ID\nErrSendPaymentFailed      // wraps the underlying banker error\nErrCurrentRealmIsNotOwner // banker called from a realm other than its owner\nErrNoOwnerProvided\nErrInvalidPaymentType     // payment routed to the wrong banker type\nErrNonCanonicalBanker     // CoinsBanker built from a non-canonical std banker\nErrNonCanonicalBankerImpl // New given a Banker of a non-canonical type\nErrSpoofedRealm           // Send called with a non-current rlm\nErrNoListerProvided\nErrGRC20TokenNotFound\n```\n\n# 4. Security\n\nThe `Banker` capability model rests on three rules:\n\n- **Construct your own bankers.** Never accept a pre-built `Banker` (including a `*WithOwner` value) from an external realm. A hostile `Balances`/`Address` can report data tied to an attacker address. `New` calls `IsCanonicalBanker` on each banker and rejects foreign types with `ErrNonCanonicalBankerImpl`.\n- **`IsCanonicalBanker` checks dynamic TYPE only, not captured state.** Embedding-based wrappers (`type Evil struct { *CoinsBanker }`) are rejected because type assertions are nominal. Any public entry point that takes a `Banker` from a caller must call it before invoking the banker's methods.\n- **Owner must match the acting realm.** `Send` asserts `rlm.IsCurrent()` (else `ErrSpoofedRealm`) and the banker rejects a caller that is not its owner (`ErrCurrentRealmIsNotOwner`). Set the owner to the realm that will actually send.\n"},{"name":"banker_coins.gno","body":"package treasury\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"errors\"\n\n\t\"gno.land/p/aeddi/panictoerr\"\n)\n\nvar ErrNonCanonicalBanker = errors.New(\"inner banker is not the canonical chain/banker.Banker\")\n\n// CoinsBanker is a Banker that sends banker.Coins.\ntype CoinsBanker struct {\n\towner  address       // The address of this coins banker owner.\n\tbanker banker.Banker // The underlying std banker, must be a BankerTypeRealmSend.\n}\n\nvar _ Banker = (*CoinsBanker)(nil)\n\n// ID implements Banker.\nfunc (CoinsBanker) ID() string {\n\treturn \"Coins\"\n}\n\n// Send implements Banker.\n//\n// rlm must be the caller's own captured cur (i.e. the cur of the\n// immediate crossing-function caller). Sending with rlm = cur.Previous()\n// or any other realm value is rejected: rlm.IsCurrent() asserts pointer\n// identity against the topmost crossing frame. Combined with the\n// rlm.Address() == cb.owner check, this restricts Send to the owning\n// realm acting in its own frame.\nfunc (cb *CoinsBanker) Send(_ int, rlm realm, p Payment) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tif rlm.Address() != cb.owner {\n\t\treturn ErrCurrentRealmIsNotOwner\n\t}\n\t// Check if payment is of type coinsPayment.\n\tpayment, ok := p.(coinsPayment)\n\tif !ok {\n\t\treturn ErrInvalidPaymentType\n\t}\n\n\t// Send the coins.\n\treturn panictoerr.PanicToError(func() {\n\t\tcb.banker.SendCoins(cb.owner, payment.toAddress, payment.coins)\n\t})\n}\n\n// Balances implements Banker.\nfunc (cb *CoinsBanker) Balances() []Balance {\n\t// Get the coins from the banker.\n\tcoins := cb.banker.GetCoins(cb.owner)\n\n\t// Convert banker.Coins to []Balance.\n\tbalances := make([]Balance, len(coins))\n\tfor i := range coins {\n\t\tbalances[i] = Balance{\n\t\t\tDenom:  coins[i].Denom,\n\t\t\tAmount: coins[i].Amount,\n\t\t}\n\t}\n\n\treturn balances\n}\n\n// Address implements Banker.\nfunc (cb *CoinsBanker) Address() string {\n\treturn cb.owner.String()\n}\n\n// NewCoinsBankerWithOwner creates a new CoinsBanker with the given address.\n//\n// banker_ must be the canonical Banker produced by banker.NewBanker;\n// hand-rolled Banker implementations (no-op fakes, decorators) are\n// rejected via banker.IsCanonical. Without this check, a callee\n// receiving a *CoinsBanker constructed from a fake banker would not\n// be able to tell that Send is a no-op (no real coins move). The\n// pkgAddr-vs-owner mismatch is still surfaced lazily by the inner\n// banker's own SendCoins check.\nfunc NewCoinsBankerWithOwner(owner address, banker_ banker.Banker) (*CoinsBanker, error) {\n\tif owner == \"\" {\n\t\treturn nil, ErrNoOwnerProvided\n\t}\n\n\tif !banker.IsCanonical(banker_) {\n\t\treturn nil, ErrNonCanonicalBanker\n\t}\n\n\treturn \u0026CoinsBanker{\n\t\towner:  owner,\n\t\tbanker: banker_,\n\t}, nil\n}\n\n// coinsPayment represents a payment that is issued by a CoinsBanker.\ntype coinsPayment struct {\n\tcoins     chain.Coins // The coins being sent.\n\ttoAddress address     // The recipient of the payment.\n}\n\nvar _ Payment = (*coinsPayment)(nil)\n\n// BankerID implements Payment.\nfunc (coinsPayment) BankerID() string {\n\treturn CoinsBanker{}.ID()\n}\n\n// String implements Payment.\nfunc (cp coinsPayment) String() string {\n\treturn cp.coins.String() + \" to \" + cp.toAddress.String()\n}\n\n// NewCoinsPayment creates a new coinsPayment.\nfunc NewCoinsPayment(coins chain.Coins, toAddress address) Payment {\n\treturn coinsPayment{\n\t\tcoins:     coins,\n\t\ttoAddress: toAddress,\n\t}\n}\n"},{"name":"banker_grc20.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tErrNoListerProvided   = errors.New(\"no lister provided\")\n\tErrGRC20TokenNotFound = errors.New(\"GRC20 token not found\")\n)\n\n// GRC20Banker is a Banker that sends GRC20 tokens listed using a getter\n// set during initialization.\ntype GRC20Banker struct {\n\towner  address         // The address of this GRC20 banker owner.\n\tlister TokenListerFunc // Allows to list tokens from methods that require it.\n}\n\n// TokenListerFunc is a function type that returns a map of GRC20 tokens.\ntype TokenListerFunc func() map[string]*grc20.Token\n\nvar _ Banker = (*GRC20Banker)(nil)\n\n// ID implements Banker.\nfunc (GRC20Banker) ID() string {\n\treturn \"GRC20\"\n}\n\n// Send implements Banker.\n//\n// rlm must be the caller's own captured cur (i.e. the cur of the\n// immediate crossing-function caller). Sending with rlm = cur.Previous()\n// or any other realm value is rejected: rlm.IsCurrent() asserts pointer\n// identity against the topmost crossing frame. Combined with the\n// rlm.Address() == gb.owner check, this restricts Send to the owning\n// realm acting in its own frame.\nfunc (gb *GRC20Banker) Send(_ int, rlm realm, p Payment) error {\n\tif !rlm.IsCurrent() {\n\t\treturn ErrSpoofedRealm\n\t}\n\tif rlm.Address() != gb.owner {\n\t\treturn ErrCurrentRealmIsNotOwner\n\t}\n\n\tpayment, ok := p.(grc20Payment)\n\tif !ok {\n\t\treturn ErrInvalidPaymentType\n\t}\n\n\t// Get the GRC20 tokens using the lister.\n\ttokens := gb.lister()\n\n\t// Look for the token corresponding to the payment tokenKey.\n\ttoken, ok := tokens[payment.tokenKey]\n\tif !ok {\n\t\treturn ufmt.Errorf(\"%v: %s\", ErrGRC20TokenNotFound, payment.tokenKey)\n\t}\n\n\t// Send the token from the owner's balance.\n\treturn token.RealmTeller(0, rlm).Transfer(0, rlm, payment.toAddress, payment.amount)\n}\n\n// Balances implements Banker.\nfunc (gb *GRC20Banker) Balances() []Balance {\n\t// Get the GRC20 tokens from the lister.\n\ttokens := gb.lister()\n\n\t// Convert GRC20 tokens to []Balance.\n\tvar balances []Balance\n\tfor key, token := range tokens {\n\t\tbalances = append(balances, Balance{\n\t\t\tDenom:  key,\n\t\t\tAmount: token.BalanceOf(gb.owner),\n\t\t})\n\t}\n\treturn balances\n}\n\n// Address implements Banker.\nfunc (gb *GRC20Banker) Address() string {\n\treturn gb.owner.String()\n}\n\n// NewGRC20BankerWithOwner creates a new GRC20Banker with the given address.\nfunc NewGRC20BankerWithOwner(owner address, lister TokenListerFunc) (*GRC20Banker, error) {\n\tif owner == \"\" {\n\t\treturn nil, ErrNoOwnerProvided\n\t}\n\n\tif lister == nil {\n\t\treturn nil, ErrNoListerProvided\n\t}\n\n\treturn \u0026GRC20Banker{\n\t\towner:  owner,\n\t\tlister: lister,\n\t}, nil\n}\n\n// grc20Payment represents a payment that is issued by a GRC20Banker.\ntype grc20Payment struct {\n\ttokenKey  string  // The key associated with the GRC20 token.\n\tamount    int64   // The amount of token to send.\n\ttoAddress address // The recipient of the payment.\n}\n\nvar _ Payment = (*grc20Payment)(nil)\n\n// BankerID implements Payment.\nfunc (grc20Payment) BankerID() string {\n\treturn GRC20Banker{}.ID()\n}\n\n// String implements Payment.\nfunc (gp grc20Payment) String() string {\n\tamount := strconv.Itoa(int(gp.amount))\n\treturn amount + gp.tokenKey + \" to \" + gp.toAddress.String()\n}\n\n// NewGRC20Payment creates a new grc20Payment.\nfunc NewGRC20Payment(tokenKey string, amount int64, toAddress address) Payment {\n\treturn grc20Payment{\n\t\ttokenKey:  tokenKey,\n\t\tamount:    amount,\n\t\ttoAddress: toAddress,\n\t}\n}\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package treasury provides treasury management for handling coin and GRC20\n// token transfers in Gno realms.\npackage treasury\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/treasury/v0\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package treasury\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/bptree/v0/pager\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tDefaultHistoryPreviewSize = 5  // Number of payments in the history preview.\n\tDefaultHistoryPageSize    = 20 // Number of payments per page in the history.\n)\n\n// Render renders content based on the given path.\nfunc (t *Treasury) Render(path string) string {\n\treturn t.router.Render(path)\n}\n\n// RenderLanding renders the landing page of the treasury.\nfunc (t *Treasury) RenderLanding(path string) string {\n\tvar out string\n\n\t// Render each banker.\n\tfor _, bankerID := range t.ListBankerIDs() {\n\t\tout += t.RenderBanker(bankerID, path)\n\t}\n\n\treturn out\n}\n\n// RenderBanker renders the details of a specific banker.\nfunc (t *Treasury) RenderBanker(bankerID string, path string) string {\n\t// Get the banker associated to this ID.\n\tbr := t.bankers.Get(bankerID)\n\tif br == nil {\n\t\treturn md.Paragraph(\"Banker not found: \" + bankerID)\n\t}\n\tbanker := br.(*bankerRecord).banker\n\n\t// Render banker title.\n\tout := md.H2(bankerID + \" Banker\")\n\n\t// Render address section.\n\tout += md.H3(\"Address\")\n\tout += md.Paragraph(banker.Address())\n\n\t// Render balances section.\n\tout += md.H3(\"Balances\")\n\tbalances := banker.Balances()\n\tif len(balances) == 0 {\n\t\tout += md.Paragraph(\"No balances found.\")\n\t} else {\n\t\ttable := mdtable.Table{Headers: []string{\"Denom\", \"Amount\"}}\n\t\tfor _, balance := range balances {\n\t\t\ttable.Append([]string{balance.Denom, strconv.FormatInt(balance.Amount, 10)})\n\t\t}\n\t\tout += table.String()\n\t}\n\n\thistorySize := DefaultHistoryPreviewSize\n\n\t// Check if the query parameter \"history_size\" is present and parse it.\n\tif req, err := url.Parse(path); err == nil \u0026\u0026 req.Query() != nil {\n\t\tsize, err := strconv.Atoi(req.Query().Get(\"history_size\"))\n\t\tif err == nil \u0026\u0026 size \u003e= 0 {\n\t\t\thistorySize = size\n\t\t}\n\t}\n\n\t// Skip history rendering if historySize is 0.\n\tif historySize == 0 {\n\t\treturn out\n\t}\n\n\t// Render history section.\n\tout += md.H3(\"History\")\n\thistory, _ := t.History(bankerID, 1, historySize)\n\tif len(history) == 0 {\n\t\tout += md.Paragraph(\"No payments sent yet.\")\n\t} else {\n\t\tif len(history) == 1 {\n\t\t\tout += md.Paragraph(\"Last payment:\")\n\t\t} else {\n\t\t\tcount := strconv.FormatInt(int64(len(history)), 10)\n\t\t\tout += md.Paragraph(\"Last \" + count + \" payments:\")\n\t\t}\n\n\t\t// Render each payment in the history.\n\t\tfor _, payment := range history {\n\t\t\tout += md.BulletItem(payment.String())\n\t\t}\n\t\tout += \"\\n\"\n\n\t\t// Build the \"See full history\" link from the owning realm's\n\t\t// path captured at New() time. Skipped if no path was supplied\n\t\t// (e.g. /p/ filetests that don't exercise rendering).\n\t\tif from := strings.IndexRune(t.realmPath, '/'); from \u003e= 0 {\n\t\t\tout += md.Link(\n\t\t\t\t\"See full history\",\n\t\t\t\tufmt.Sprintf(\"%s:%s/history\", t.realmPath[from:], bankerID),\n\t\t\t)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// RenderBankerHistory renders the payment history of a specific banker.\nfunc (t *Treasury) RenderBankerHistory(bankerID string, path string) string {\n\t// Get the banker record corresponding to this ID if it exists.\n\tbr := t.bankers.Get(bankerID)\n\tif br == nil {\n\t\treturn md.Paragraph(\"Banker not found: \" + bankerID)\n\t}\n\thistory := br.(*bankerRecord).history\n\n\t// Render banker history title.\n\tout := md.H2(bankerID + \" Banker History\")\n\n\t// Get the current page of tokens based on the request path.\n\tp := pager.NewPager(history.Tree(), DefaultHistoryPageSize, true)\n\tpage, err := p.GetPageByPath(path)\n\tif err != nil {\n\t\treturn md.Paragraph(\"Error retrieving page: \" + err.Error())\n\t}\n\n\t// Render full history section.\n\tif history.Len() == 0 {\n\t\tout += md.Paragraph(\"No payments sent yet.\")\n\t} else {\n\t\tif history.Len() == 1 {\n\t\t\tout += md.Paragraph(\"1 payment:\")\n\t\t} else {\n\t\t\tcount := strconv.FormatInt(int64(history.Len()), 10)\n\t\t\tout += md.Paragraph(count + \" payments (sorted by latest, descending):\")\n\t\t}\n\t\tfor _, item := range page.Items {\n\t\t\tout += md.BulletItem(item.Value.(Payment).String())\n\t\t}\n\t}\n\tout += \"\\n\"\n\n\t// Add the page picker.\n\tout += md.Paragraph(page.Picker(path))\n\n\treturn out\n}\n\n// initRenderRouter registers the routes for rendering the treasury pages.\nfunc (t *Treasury) initRenderRouter() {\n\tt.router = mux.NewRouter()\n\n\t// Landing page.\n\tt.router.HandleFunc(\"\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(t.RenderLanding(req.RawPath))\n\t})\n\n\t// Banker details.\n\tt.router.HandleFunc(\"{banker}\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(t.RenderBanker(req.GetVar(\"banker\"), req.RawPath))\n\t})\n\n\t// Banker full history.\n\tt.router.HandleFunc(\"{banker}/history\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(t.RenderBankerHistory(req.GetVar(\"banker\"), req.RawPath))\n\t})\n}\n"},{"name":"treasury.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/bptree/v0/pager\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tErrNoBankerProvided       = errors.New(\"no banker provided\")\n\tErrDuplicateBanker        = errors.New(\"duplicate banker\")\n\tErrBankerNotFound         = errors.New(\"banker not found\")\n\tErrSendPaymentFailed      = errors.New(\"failed to send payment\")\n\tErrNonCanonicalBankerImpl = errors.New(\"non-canonical Banker impl: only *CoinsBanker / *GRC20Banker accepted\")\n)\n\n// New creates a new Treasury instance with the given bankers.\n//\n// pkgPath should be the package path of the owning realm. It is\n// captured on the Treasury and used to build the \"See full history\"\n// link in RenderBanker. Pass `cur.PkgPath()` from the owning realm's\n// init (or another live-cur context). Passing \"\" disables the link.\n//\n// The path is stored as plain data — no IsCurrent guard inside this\n// /p/ constructor. The caller is the trust boundary; it must derive\n// the value from a real `cur realm` rather than accept it from\n// untrusted input. The captured path is consumed only for render\n// output (Class-2 designation-forgery shape, accepted as\n// display-only — see docs/resources/gno-security.md).\n//\n// Each banker must be one of treasury's canonical concrete impls\n// (*CoinsBanker, *GRC20Banker) per IsCanonicalBanker. Foreign-realm impls\n// (including embedded wrappers around canonical types) are rejected: a\n// malicious Send impl would receive a capability token via its rlm\n// parameter when treasury.Send dispatches into it.\n//\n// The allowlist validates type only, not captured state. Treasury operators\n// must construct their own bankers; never accept a pre-built *Banker value\n// from an external realm.\nfunc New(bankers []Banker, pkgPath string) (*Treasury, error) {\n\tif len(bankers) == 0 {\n\t\treturn nil, ErrNoBankerProvided\n\t}\n\n\t// Canonical-impl allowlist: reject foreign types (embedding-based\n\t// bypasses fail here because type assertions are nominal).\n\tfor _, b := range bankers {\n\t\tif !IsCanonicalBanker(b) {\n\t\t\treturn nil, ErrNonCanonicalBankerImpl\n\t\t}\n\t}\n\n\t// Create a new Treasury instance.\n\ttreasury := \u0026Treasury{bankers: bptree.NewBPTree32(), realmPath: pkgPath}\n\n\t// Register the bankers.\n\tfor _, banker := range bankers {\n\t\tif treasury.bankers.Has(banker.ID()) {\n\t\t\treturn nil, ufmt.Errorf(\"%v: %s\", ErrDuplicateBanker, banker.ID())\n\t\t}\n\n\t\ttreasury.bankers.Set(\n\t\t\tbanker.ID(),\n\t\t\t\u0026bankerRecord{banker: banker},\n\t\t)\n\t}\n\n\t// Register the Render routes.\n\ttreasury.initRenderRouter()\n\n\treturn treasury, nil\n}\n\n// Send sends a payment using the corresponding banker. rlm is threaded\n// to the banker's Send for IsCurrent + owner validation.\nfunc (t *Treasury) Send(_ int, rlm realm, p Payment) error {\n\t// Get the banker record corresponding to this Payment.\n\tbr := t.bankers.Get(p.BankerID())\n\tif br == nil {\n\t\treturn ufmt.Errorf(\"%v: %s\", ErrBankerNotFound, p.BankerID())\n\t}\n\trecord := br.(*bankerRecord)\n\n\t// Send the payment using the corresponding banker.\n\tif err := record.banker.Send(0, rlm, p); err != nil {\n\t\treturn ufmt.Errorf(\"%v: %s\", ErrSendPaymentFailed, err)\n\t}\n\n\t// Add the payment to the history of the banker.\n\trecord.history.Append(p)\n\n\treturn nil\n}\n\n// History returns the payment history sent by the banker with the given ID.\n// Payments are paginated, with the most recent payments first.\nfunc (t *Treasury) History(\n\tbankerID string,\n\tpageNumber int,\n\tpageSize int,\n) ([]Payment, error) {\n\t// Get the banker record corresponding to this ID.\n\tbr := t.bankers.Get(bankerID)\n\tif br == nil {\n\t\treturn nil, ufmt.Errorf(\"%v: %s\", ErrBankerNotFound, bankerID)\n\t}\n\thistory := br.(*bankerRecord).history\n\n\t// Get the page of payments from the history.\n\tp := pager.NewPager(history.Tree(), pageSize, true)\n\tpage := p.GetPage(pageNumber)\n\n\t// Convert the items in the page to a slice of Payments.\n\tpayments := make([]Payment, len(page.Items))\n\tfor i := range page.Items {\n\t\tpayments[i] = page.Items[i].Value.(Payment)\n\t}\n\n\treturn payments, nil\n}\n\n// Balances returns the balances of the banker with the given ID.\nfunc (t *Treasury) Balances(bankerID string) ([]Balance, error) {\n\t// Get the banker record corresponding to this ID.\n\tbr := t.bankers.Get(bankerID)\n\tif br == nil {\n\t\treturn nil, ufmt.Errorf(\"%v: %s\", ErrBankerNotFound, bankerID)\n\t}\n\n\t// Get the balances from the banker.\n\treturn br.(*bankerRecord).banker.Balances(), nil\n}\n\n// Address returns the address of the banker with the given ID.\nfunc (t *Treasury) Address(bankerID string) (string, error) {\n\t// Get the banker record corresponding to this ID.\n\tbr := t.bankers.Get(bankerID)\n\tif br == nil {\n\t\treturn \"\", ufmt.Errorf(\"%v: %s\", ErrBankerNotFound, bankerID)\n\t}\n\n\t// Get the address from the banker.\n\treturn br.(*bankerRecord).banker.Address(), nil\n}\n\n// HasBanker checks if a banker with the given ID is registered.\nfunc (t *Treasury) HasBanker(bankerID string) bool {\n\treturn t.bankers.Has(bankerID)\n}\n\n// ListBankerIDs returns a list of all registered banker IDs.\nfunc (t *Treasury) ListBankerIDs() []string {\n\tvar bankerIDs []string\n\n\tt.bankers.Iterate(\"\", \"\", func(bankerID string, _ any) bool {\n\t\tbankerIDs = append(bankerIDs, bankerID)\n\t\treturn false\n\t})\n\n\treturn bankerIDs\n}\n"},{"name":"types.gno","body":"package treasury\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/bptree/v0/list\"\n\t\"gno.land/p/nt/mux/v0\"\n)\n\n// Treasury is the main structure that holds all bankers and their payment\n// history. It also provides a router for rendering the treasury pages.\ntype Treasury struct {\n\tbankers   *bptree.BPTree // string -\u003e *bankerRecord\n\trouter    *mux.Router\n\trealmPath string // owning realm's PkgPath, captured at New() (used for render links)\n}\n\n// bankerRecord holds a Banker and its payment history.\ntype bankerRecord struct {\n\tbanker  Banker\n\thistory list.List // List of Payment.\n}\n\n// Banker is an interface that allows for banking operations.\n//\n// SECURITY: Send takes (int, realm, Payment), so handing a Banker value to\n// untrusted code yields a capability token to whatever Send impl that code\n// dispatches into. The set of canonical impls is closed (*CoinsBanker,\n// *GRC20Banker); any public function that accepts a Banker as a parameter\n// from external callers MUST verify it via IsCanonicalBanker and reject\n// otherwise. treasury.New enforces this for its own intake; future\n// Banker-accepting APIs must do the same. An unexported-marker \"seal\" does\n// NOT defend against this — see\n// p/test/seal/filetests/z_seal_iface_embedding_filetest.gno.\n//\n// Note that IsCanonicalBanker validates dynamic TYPE only, not captured\n// STATE: a canonical *CoinsBanker constructed via NewCoinsBankerWithOwner\n// with a hostile owner argument passes the allowlist but its read methods\n// (Balances, Address) report data tied to that hostile address. Treasury\n// operators must construct their own bankers and NEVER accept pre-built\n// *Banker values from external realms.\ntype Banker interface {\n\tID() string                     // Get the ID of the banker.\n\tSend(int, realm, Payment) error // Send a payment to a recipient.\n\tBalances() []Balance            // Get the balances of the banker.\n\tAddress() string                // Get the address of the banker to receive payments.\n}\n\n// IsCanonicalBanker reports whether b is one of treasury's canonical\n// concrete Banker impls. Use this at any public entry point in /p/ or /r/\n// that accepts a Banker from an external caller before invoking its methods.\n//\n// Foreign types — including embedding-based wrappers like\n// `type Evil struct { *CoinsBanker }` — are rejected because type assertions\n// are nominal: *Evil is not *CoinsBanker, regardless of method promotion.\n//\n// To add a new canonical type: extend the switch below AND add a regression\n// test (under filetests/ in this package) that an embedded-impl bypass is\n// rejected.\n//\n// Mirrors the precedent of chain/banker.IsCanonical and\n// p/jaekwon/allowancesender's canonical-impl check.\nfunc IsCanonicalBanker(b Banker) bool {\n\tswitch b.(type) {\n\tcase *CoinsBanker, *GRC20Banker:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Payment is an interface that allows getting details about a payment.\ntype Payment interface {\n\tBankerID() string // Get the ID of the banker that can process this payment.\n\tString() string   // Get a string representation of the payment.\n}\n\n// Balance represents the balance of an asset held by a Banker.\ntype Balance struct {\n\tDenom  string // The denomination of the asset\n\tAmount int64  // The amount of the asset\n}\n\n// Common Banker errors.\nvar (\n\tErrCurrentRealmIsNotOwner = errors.New(\"current realm is not the owner of the banker\")\n\tErrNoOwnerProvided        = errors.New(\"no owner provided\")\n\tErrInvalidPaymentType     = errors.New(\"invalid payment type\")\n\tErrSpoofedRealm           = errors.New(\"rlm does not match the current crossing frame\")\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"svg","path":"gno.land/p/demo/svg","files":[{"name":"doc.gno","body":"/*\nPackage svg is a minimalist and extensible SVG generation library for Gno.\n\nIt provides a structured way to create and compose SVG elements such as rectangles, circles, text, paths, and more. The package is designed to be modular and developer-friendly, enabling optional attributes and method chaining for ease of use.\n\nEach SVG element embeds a BaseAttrs struct, which supports common SVG attributes like `id`, `class`, `style`, `fill`, `stroke`, and `transform`.\n\nCanvas objects represent the root SVG container and support global dimensions, viewBox configuration, embedded styles, and element composition.\n\nExample:\n\n\timport \"gno.land/p/demo/svg\"\n\n\tfunc Foo() string {\n\t\tcanvas := svg.NewCanvas(200, 200).WithViewBox(0, 0, 200, 200)\n\t\tcanvas.AddStyle(\".my-rect\", \"stroke:black;stroke-width:2\")\n\t\tcanvas.Append(\n\t\t\tsvg.NewRectangle(60, 40, 100, 50, \"red\").WithClass(\"my-rect\"),\n\t\t\tsvg.NewCircle(50, 80, 40, \"blue\"),\n\t\t\t\u0026svg.Path{D: `M 10,30\n\t\t\tA 20,20 0,0,1 50,30\n\t\t\t\tA 20,20 0,0,1  90,30\n\t\t\t\tQ 90,60 50,90\n\t\t\t\tQ 10,60 10,30 z`, Fill: \"magenta\"},\n\t\t\tsvg.NewText(20, 50, \"Hello SVG\", \"black\"),\n\t\t)\n\t\tmysvg := canvas.Base64()\n\t}\n*/\npackage svg // import \"gno.land/p/demo/svg\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/svg\"\ngno = \"0.9\"\n"},{"name":"svg.gno","body":"package svg\n\nimport (\n\t\"encoding/base64\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Canvas struct {\n\tWidth, Height int\n\tViewBox       string\n\tElems         []Elem\n\tStyle         *avl.Tree\n}\n\ntype Elem interface{ String() string }\n\nfunc NewCanvas(width, height int) *Canvas {\n\treturn \u0026Canvas{\n\t\tWidth:  width,\n\t\tHeight: height,\n\t\tStyle:  nil,\n\t}\n}\n\nfunc (c *Canvas) AddStyle(key, value string) *Canvas {\n\tif c.Style == nil {\n\t\tc.Style = avl.NewTree()\n\t}\n\tc.Style.Set(key, value)\n\treturn c\n}\n\nfunc (c *Canvas) WithViewBox(x, y, width, height int) *Canvas {\n\tc.ViewBox = ufmt.Sprintf(\"%d %d %d %d\", x, y, width, height)\n\treturn c\n}\n\n// Render renders your canvas\nfunc (c Canvas) Render(alt string) string {\n\tbase64SVG := base64.StdEncoding.EncodeToString([]byte(c.String()))\n\treturn ufmt.Sprintf(\"![%s](data:image/svg+xml;base64,%s)\", alt, base64SVG)\n}\n\nfunc (c Canvas) String() string {\n\tout := \"\"\n\tout += ufmt.Sprintf(`\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"%s\"\u003e`, c.Width, c.Height, c.ViewBox)\n\tif c.Style != nil {\n\t\tout += \"\u003cstyle\u003e\"\n\t\tc.Style.Iterate(\"\", \"\", func(k string, val interface{}) bool {\n\t\t\tv := val.(string)\n\t\t\tout += ufmt.Sprintf(\"%s{%s}\", k, v)\n\t\t\treturn false\n\t\t})\n\t\tout += \"\u003c/style\u003e\"\n\t}\n\tfor _, elem := range c.Elems {\n\t\tout += elem.String()\n\t}\n\tout += \"\u003c/svg\u003e\"\n\treturn out\n}\n\nfunc (c Canvas) Base64() string {\n\tout := c.String()\n\treturn base64.StdEncoding.EncodeToString([]byte(out))\n}\n\nfunc (c *Canvas) Append(elem ...Elem) {\n\tc.Elems = append(c.Elems, elem...)\n}\n\ntype BaseAttrs struct {\n\tID          string\n\tClass       string\n\tStyle       string\n\tStroke      string\n\tStrokeWidth string\n\tOpacity     string\n\tTransform   string\n\tVisibility  string\n}\n\nfunc (b BaseAttrs) String() string {\n\tvar elems []string\n\n\tif b.ID != \"\" {\n\t\telems = append(elems, `id=\"`+b.ID+`\"`)\n\t}\n\tif b.Class != \"\" {\n\t\telems = append(elems, `class=\"`+b.Class+`\"`)\n\t}\n\tif b.Style != \"\" {\n\t\telems = append(elems, `style=\"`+b.Style+`\"`)\n\t}\n\tif b.Stroke != \"\" {\n\t\telems = append(elems, `stroke=\"`+b.Stroke+`\"`)\n\t}\n\tif b.StrokeWidth != \"\" {\n\t\telems = append(elems, `stroke-width=\"`+b.StrokeWidth+`\"`)\n\t}\n\tif b.Opacity != \"\" {\n\t\telems = append(elems, `opacity=\"`+b.Opacity+`\"`)\n\t}\n\tif b.Transform != \"\" {\n\t\telems = append(elems, `transform=\"`+b.Transform+`\"`)\n\t}\n\tif b.Visibility != \"\" {\n\t\telems = append(elems, `visibility=\"`+b.Visibility+`\"`)\n\t}\n\tif len(elems) == 0 {\n\t\treturn \"\"\n\t}\n\treturn strings.Join(elems, \" \")\n}\n\ntype Circle struct {\n\tCX   int // center X\n\tCY   int // center Y\n\tR    int // radius\n\tFill string\n\tAttr BaseAttrs\n}\n\nfunc (c Circle) String() string {\n\treturn ufmt.Sprintf(`\u003ccircle cx=\"%d\" cy=\"%d\" r=\"%d\" fill=\"%s\" %s/\u003e`, c.CX, c.CY, c.R, c.Fill, c.Attr.String())\n}\n\nfunc NewCircle(cx, cy, r int, fill string) *Circle {\n\treturn \u0026Circle{\n\t\tCX:   cx,\n\t\tCY:   cy,\n\t\tR:    r,\n\t\tFill: fill,\n\t}\n}\n\nfunc (c *Circle) WithClass(class string) *Circle {\n\tc.Attr.Class = class\n\treturn c\n}\n\ntype Ellipse struct {\n\tCX   int // center X\n\tCY   int // center Y\n\tRX   int // radius X\n\tRY   int // radius Y\n\tFill string\n\tAttr BaseAttrs\n}\n\nfunc (e Ellipse) String() string {\n\treturn ufmt.Sprintf(`\u003cellipse cx=\"%d\" cy=\"%d\" rx=\"%d\" ry=\"%d\" fill=\"%s\" %s/\u003e`, e.CX, e.CY, e.RX, e.RY, e.Fill, e.Attr.String())\n}\n\nfunc NewEllipse(cx, cy int, fill string) *Ellipse {\n\treturn \u0026Ellipse{\n\t\tCX:   cx,\n\t\tCY:   cy,\n\t\tFill: fill,\n\t}\n}\n\nfunc (e *Ellipse) WithClass(class string) *Ellipse {\n\te.Attr.Class = class\n\treturn e\n}\n\ntype Rectangle struct {\n\tX, Y, Width, Height int\n\tRX, RY              int // corner radiuses\n\tFill                string\n\tAttr                BaseAttrs\n}\n\nfunc (r Rectangle) String() string {\n\treturn ufmt.Sprintf(`\u003crect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" rx=\"%d\" ry=\"%d\" fill=\"%s\" %s/\u003e`, r.X, r.Y, r.Width, r.Height, r.RX, r.RY, r.Fill, r.Attr.String())\n}\n\nfunc NewRectangle(x, y, width, height int, fill string) *Rectangle {\n\treturn \u0026Rectangle{\n\t\tX:      x,\n\t\tY:      y,\n\t\tWidth:  width,\n\t\tHeight: height,\n\t\tFill:   fill,\n\t}\n}\n\nfunc (r *Rectangle) WithClass(class string) *Rectangle {\n\tr.Attr.Class = class\n\treturn r\n}\n\ntype Path struct {\n\tD    string\n\tFill string\n\tAttr BaseAttrs\n}\n\nfunc (p Path) String() string {\n\treturn ufmt.Sprintf(`\u003cpath d=\"%s\" fill=\"%s\" %s/\u003e`, p.D, p.Fill, p.Attr.String())\n}\n\nfunc NewPath(d, fill string) *Path {\n\treturn \u0026Path{\n\t\tD:    d,\n\t\tFill: fill,\n\t}\n}\n\nfunc (p *Path) WithClass(class string) *Path {\n\tp.Attr.Class = class\n\treturn p\n}\n\ntype Polygon struct { // closed shape\n\tPoints string\n\tFill   string\n\tAttr   BaseAttrs\n}\n\nfunc (p Polygon) String() string {\n\treturn ufmt.Sprintf(`\u003cpolygon points=\"%s\" fill=\"%s\" %s/\u003e`, p.Points, p.Fill, p.Attr.String())\n}\n\nfunc NewPolygon(points, fill string) *Polygon {\n\treturn \u0026Polygon{\n\t\tPoints: points,\n\t\tFill:   fill,\n\t}\n}\n\nfunc (p *Polygon) WithClass(class string) *Polygon {\n\tp.Attr.Class = class\n\treturn p\n}\n\ntype Polyline struct { // polygon but not necessarily closed\n\tPoints string\n\tFill   string\n\tAttr   BaseAttrs\n}\n\nfunc (p Polyline) String() string {\n\treturn ufmt.Sprintf(`\u003cpolyline points=\"%s\" fill=\"%s\" %s/\u003e`, p.Points, p.Fill, p.Attr.String())\n}\n\nfunc NewPolyline(points, fill string) *Polyline {\n\treturn \u0026Polyline{\n\t\tPoints: points,\n\t\tFill:   fill,\n\t}\n}\n\nfunc (p *Polyline) WithClass(class string) *Polyline {\n\tp.Attr.Class = class\n\treturn p\n}\n\ntype Text struct {\n\tX, Y       int\n\tDX, DY     int // shift text pos horizontally/ vertically\n\tRotate     string\n\tText, Fill string\n\tAttr       BaseAttrs\n}\n\nfunc (c Text) String() string {\n\treturn ufmt.Sprintf(`\u003ctext x=\"%d\" y=\"%d\" dx=\"%d\" dy=\"%d\" rotate=\"%s\" fill=\"%s\" %s\u003e%s\u003c/text\u003e`, c.X, c.Y, c.DX, c.DY, c.Rotate, c.Fill, c.Attr.String(), c.Text)\n}\n\nfunc NewText(x, y int, text, fill string) *Text {\n\treturn \u0026Text{\n\t\tX:    x,\n\t\tY:    y,\n\t\tText: text,\n\t\tFill: fill,\n\t}\n}\n\nfunc (c *Text) WithClass(class string) *Text {\n\tc.Attr.Class = class\n\treturn c\n}\n\ntype Group struct {\n\tElems []Elem\n\tFill  string\n\tAttr  BaseAttrs\n}\n\nfunc (g Group) String() string {\n\tout := \"\"\n\tfor _, e := range g.Elems {\n\t\tout += e.String()\n\t}\n\treturn ufmt.Sprintf(`\u003cg fill=\"%s\" %s\u003e%s\u003c/g\u003e`, g.Fill, g.Attr.String(), out)\n}\n\nfunc NewGroup(fill string) *Group {\n\treturn \u0026Group{\n\t\tFill: fill,\n\t}\n}\n\nfunc (g *Group) Append(elem ...Elem) {\n\tg.Elems = append(g.Elems, elem...)\n}\n\nfunc (g *Group) WithClass(class string) *Group {\n\tg.Attr.Class = class\n\treturn g\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"md","path":"gno.land/p/sunspirit/md","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/sunspirit/md\"\ngno = \"0.9\"\n"},{"name":"md.gno","body":"package md\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Builder helps to build a Markdown string from individual elements\ntype Builder struct {\n\telements []string\n}\n\n// NewBuilder creates a new Builder instance\nfunc NewBuilder() *Builder {\n\treturn \u0026Builder{}\n}\n\n// Add adds a Markdown element to the builder\nfunc (m *Builder) Add(md ...string) *Builder {\n\tm.elements = append(m.elements, md...)\n\treturn m\n}\n\n// Render returns the final Markdown string joined with the specified separator\nfunc (m *Builder) Render(separator string) string {\n\treturn strings.Join(m.elements, separator)\n}\n\n// Bold returns bold text for markdown\nfunc Bold(text string) string {\n\treturn ufmt.Sprintf(\"**%s**\", text)\n}\n\n// Italic returns italicized text for markdown\nfunc Italic(text string) string {\n\treturn ufmt.Sprintf(\"*%s*\", text)\n}\n\n// Strikethrough returns strikethrough text for markdown\nfunc Strikethrough(text string) string {\n\treturn ufmt.Sprintf(\"~~%s~~\", text)\n}\n\n// H1 returns a level 1 header for markdown\nfunc H1(text string) string {\n\treturn ufmt.Sprintf(\"# %s\\n\", text)\n}\n\n// H2 returns a level 2 header for markdown\nfunc H2(text string) string {\n\treturn ufmt.Sprintf(\"## %s\\n\", text)\n}\n\n// H3 returns a level 3 header for markdown\nfunc H3(text string) string {\n\treturn ufmt.Sprintf(\"### %s\\n\", text)\n}\n\n// H4 returns a level 4 header for markdown\nfunc H4(text string) string {\n\treturn ufmt.Sprintf(\"#### %s\\n\", text)\n}\n\n// H5 returns a level 5 header for markdown\nfunc H5(text string) string {\n\treturn ufmt.Sprintf(\"##### %s\\n\", text)\n}\n\n// H6 returns a level 6 header for markdown\nfunc H6(text string) string {\n\treturn ufmt.Sprintf(\"###### %s\\n\", text)\n}\n\n// BulletList returns an bullet list for markdown\nfunc BulletList(items []string) string {\n\tvar sb strings.Builder\n\tfor _, item := range items {\n\t\tsb.WriteString(ufmt.Sprintf(\"- %s\\n\", item))\n\t}\n\treturn sb.String()\n}\n\n// OrderedList returns an ordered list for markdown\nfunc OrderedList(items []string) string {\n\tvar sb strings.Builder\n\tfor i, item := range items {\n\t\tsb.WriteString(ufmt.Sprintf(\"%d. %s\\n\", i+1, item))\n\t}\n\treturn sb.String()\n}\n\n// TodoList returns a list of todo items with checkboxes for markdown\nfunc TodoList(items []string, done []bool) string {\n\tvar sb strings.Builder\n\n\tfor i, item := range items {\n\t\tcheckbox := \" \"\n\t\tif done[i] {\n\t\t\tcheckbox = \"x\"\n\t\t}\n\t\tsb.WriteString(ufmt.Sprintf(\"- [%s] %s\\n\", checkbox, item))\n\t}\n\treturn sb.String()\n}\n\n// Blockquote returns a blockquote for markdown\nfunc Blockquote(text string) string {\n\tlines := strings.Split(text, \"\\n\")\n\tvar sb strings.Builder\n\tfor _, line := range lines {\n\t\tsb.WriteString(ufmt.Sprintf(\"\u003e %s\\n\", line))\n\t}\n\n\treturn sb.String()\n}\n\n// InlineCode returns inline code for markdown\nfunc InlineCode(code string) string {\n\treturn ufmt.Sprintf(\"`%s`\", code)\n}\n\n// CodeBlock creates a markdown code block\nfunc CodeBlock(content string) string {\n\treturn ufmt.Sprintf(\"```\\n%s\\n```\", content)\n}\n\n// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting\nfunc LanguageCodeBlock(language, content string) string {\n\treturn ufmt.Sprintf(\"```%s\\n%s\\n```\", language, content)\n}\n\n// LineBreak returns the specified number of line breaks for markdown\nfunc LineBreak(count uint) string {\n\tif count \u003e 0 {\n\t\treturn strings.Repeat(\"\\n\", int(count)+1)\n\t}\n\treturn \"\"\n}\n\n// HorizontalRule returns a horizontal rule for markdown\nfunc HorizontalRule() string {\n\treturn \"---\\n\"\n}\n\n// Link returns a hyperlink for markdown\nfunc Link(text, url string) string {\n\treturn ufmt.Sprintf(\"[%s](%s)\", text, url)\n}\n\n// Image returns an image for markdown\nfunc Image(altText, url string) string {\n\treturn ufmt.Sprintf(\"![%s](%s)\", altText, url)\n}\n\n// Footnote returns a footnote for markdown\nfunc Footnote(reference, text string) string {\n\treturn ufmt.Sprintf(\"[%s]: %s\", reference, text)\n}\n\n// Paragraph wraps the given text in a Markdown paragraph\nfunc Paragraph(content string) string {\n\treturn ufmt.Sprintf(\"%s\\n\", content)\n}\n\n// MdTable is an interface for table types that can be converted to Markdown format\ntype MdTable interface {\n\tString() string\n}\n\n// Table takes any MdTable implementation and returns its markdown representation\nfunc Table(table MdTable) string {\n\treturn table.String()\n}\n\n// EscapeMarkdown escapes special markdown characters in a string\nfunc EscapeMarkdown(text string) string {\n\treturn ufmt.Sprintf(\"``%s``\", text)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"piechart","path":"gno.land/p/samcrew/piechart","files":[{"name":"README.md","body":"# `piechart` - SVG pie charts \n\nGenerate pie charts with legends as SVG markup for gnoweb rendering.\n\n## Usage\n\n```go\nslices := []piechart.PieSlice{\n    {Value: 30, Color: \"#ff6b6b\", Label: \"Frontend\"},\n    {Value: 25, Color: \"#4ecdc4\", Label: \"Backend\"},\n    {Value: 20, Color: \"#45b7d1\", Label: \"DevOps\"},\n    {Value: 15, Color: \"#96ceb4\", Label: \"Mobile\"},\n    {Value: 10, Color: \"#ffeaa7\", Label: \"Other\"},\n}\n\n// With title\ntitledChart := piechart.Render(slices, \"Team Distribution\")\n\n// Without title  \nuntitledChart := piechart.Render(slices, \"\")\n```\n\n## API Reference\n\n```go\ntype PieSlice struct {\n    Value float64 // Numeric value for the slice\n    Color string  // Hex color code (e.g., \"#ff6b6b\")\n    Label string  // Display label for the slice\n}\n\n// slices: Array of PieSlice structs containing the data\n// title: Chart title (empty string for no title)\n// Returns: SVG markup as a string\nfunc Render(slices []PieSlice, title string) string\n```\n\n## Live Example\n\n- [/r/docs/charts:piechart](/r/docs/charts:piechart)\n- [/r/samcrew/daodemo/custom_condition:members](/r/samcrew/daodemo/custom_condition:members)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/piechart\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"},{"name":"piechart.gno","body":"// Package piechart provides functionality to render a pie chart as an SVG image.\n// It takes a list of PieSlice objects, each representing a slice of the pie with a value,\n// color, and label, and generates an SVG representation of the pie chart.\npackage piechart\n\nimport (\n\t\"math\"\n\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sunspirit/md\"\n)\n\ntype PieSlice struct {\n\tValue float64\n\tColor string\n\tLabel string\n}\n\n// Render creates an SVG pie chart from given slices (value, color and label).\n// It returns an img svg markup as a string, including a markdown header if a non-empty title is provided.\nfunc Render(slices []PieSlice, title string) string {\n\t// Validate input slices length\n\tif len(slices) == 0 {\n\t\treturn \"\\npiechart fails: no data provided\"\n\t}\n\n\tconst (\n\t\tcanvasWidth  = 500\n\t\tcanvasHeight = 200\n\t\tcenterX      = 100.0\n\t\tcenterY      = 100.0\n\t\tradius       = 80.0\n\t\tlegendX      = 210\n\t\tlegendStartY = 30\n\t\tlineHeight   = 26\n\t\tsquareSize   = 16\n\t\tfontSize     = 16\n\t)\n\n\tcanvas := svg.NewCanvas(canvasWidth, canvasHeight)\n\n\t// Sum all values to compute slices proportions\n\tvar total float64\n\tfor _, s := range slices {\n\t\ttotal += s.Value\n\t}\n\n\t// Draw pie slices and legend in one pass\n\tstartAngle := -math.Pi / 2\n\tfor i, s := range slices {\n\t\tif s.Value \u003e 0 {\n\t\t\t// --- PIE SLICE ---\n\t\t\t// Calculate angle span for current slice\n\t\t\tangle := (s.Value / total) * 2 * math.Pi\n\t\t\tendAngle := startAngle + angle\n\n\t\t\t// Compute start and end points on the circle circumference\n\t\t\tcosStart, sinStart := math.Cos(startAngle), math.Sin(startAngle)\n\t\t\tcosEnd, sinEnd := math.Cos(endAngle), math.Sin(endAngle)\n\t\t\tx1 := centerX + radius*cosStart\n\t\t\ty1 := centerY + radius*sinStart\n\t\t\tx2 := centerX + radius*cosEnd\n\t\t\ty2 := centerY + radius*sinEnd\n\n\t\t\t// Determine if the arc should be a large arc (\u003e 180 degrees) (Arc direction)\n\t\t\tlargeArcFlag := 0\n\t\t\tif angle \u003e math.Pi {\n\t\t\t\tlargeArcFlag = 1\n\t\t\t}\n\n\t\t\t// Build the SVG path for the pie slice\n\t\t\tpath := ufmt.Sprintf(\n\t\t\t\t\"M%.2f,%.2f L%.2f,%.2f A%.2f,%.2f 0 %d 1 %.2f,%.2f Z\",\n\t\t\t\tcenterX, centerY, x1, y1, radius, radius, largeArcFlag, x2, y2,\n\t\t\t)\n\n\t\t\t// Colored slice\n\t\t\tcanvas.Append(svg.Path{\n\t\t\t\tD:    path,\n\t\t\t\tFill: s.Color,\n\t\t\t})\n\n\t\t\tstartAngle = endAngle\n\t\t}\n\n\t\t// --- LEGEND ---\n\t\ty := legendStartY + i*lineHeight\n\t\t// Colored square representing slice color\n\t\tcanvas.Append(svg.Rectangle{\n\t\t\tX:      legendX,\n\t\t\tY:      y - squareSize/2,\n\t\t\tWidth:  squareSize,\n\t\t\tHeight: squareSize,\n\t\t\tFill:   s.Color,\n\t\t})\n\n\t\t// Legend text showing label, value and percentage\n\t\ttext := ufmt.Sprintf(\"%s: %.0f (%.1f%%)\", s.Label, s.Value, s.Value*100/total)\n\t\tcanvas.Append(svg.Text{\n\t\t\tX:    legendX + squareSize + 8,\n\t\t\tY:    y + fontSize/3,\n\t\t\tText: text,\n\t\t\tFill: \"#54595D\",\n\t\t\tAttr: svg.BaseAttrs{\n\t\t\t\tStyle: ufmt.Sprintf(\"font-family:'Inter var',sans-serif;font-size:%dpx;\", fontSize),\n\t\t\t},\n\t\t})\n\t}\n\n\tif title == \"\" {\n\t\treturn canvas.Render(\"Pie Chart\")\n\t}\n\treturn md.H2(title) + canvas.Render(\"Pie Chart \"+title)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"md","path":"gno.land/p/mason/md","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/mason/md\"\ngno = \"0.9\"\n"},{"name":"md.gno","body":"package md\n\nimport (\n\t\"strings\"\n)\n\ntype MD struct {\n\telements []string\n}\n\nfunc New() *MD {\n\treturn \u0026MD{elements: []string{}}\n}\n\nfunc (m *MD) H1(text string) {\n\tm.elements = append(m.elements, \"# \"+text)\n}\n\nfunc (m *MD) H3(text string) {\n\tm.elements = append(m.elements, \"### \"+text)\n}\n\nfunc (m *MD) P(text string) {\n\tm.elements = append(m.elements, text)\n}\n\nfunc (m *MD) Code(text string) {\n\tm.elements = append(m.elements, \"  ```\\n\"+text+\"\\n```\\n\")\n}\n\nfunc (m *MD) Im(path string, caption string) {\n\tm.elements = append(m.elements, \"![\"+caption+\"](\"+path+\" \\\"\"+caption+\"\\\")\")\n}\n\nfunc (m *MD) Bullet(point string) {\n\tm.elements = append(m.elements, \"- \"+point)\n}\n\nfunc Link(text, url string, title ...string) string {\n\tif len(title) \u003e 0 \u0026\u0026 title[0] != \"\" {\n\t\treturn \"[\" + text + \"](\" + url + \" \\\"\" + title[0] + \"\\\")\"\n\t}\n\treturn \"[\" + text + \"](\" + url + \")\"\n}\n\nfunc (m *MD) Render() string {\n\treturn strings.Join(m.elements, \"\\n\\n\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tablesort","path":"gno.land/p/samcrew/tablesort","files":[{"name":"README.md","body":"# `tablesort` - Sortable markdown tables\n\nGenerate sortable markdown tables with clickable column headers. Sorting state is managed via URL query parameters.\n\n## Usage\n\n```go\nimport \"gno.land/p/samcrew/tablesort\"\n\ntable := \u0026tablesort.Table{\n    Headings: []string{\"Name\", \"Age\", \"City\"},\n    Rows: [][]string{\n        {\"Alice\", \"25\", \"New York\"},\n        {\"Bob\", \"30\", \"London\"},\n        {\"Charlie\", \"22\", \"Paris\"},\n    },\n}\n\n// Basic usage\nu, _ := url.Parse(\"/users\")\nmarkdown := tablesort.Render(u, table, \"\")\n\n// Multiple tables on same page (use prefix to avoid conflicts)\nmarkdown1 := tablesort.Render(u, table, \"table1-\")\nmarkdown2 := tablesort.Render(u, table, \"table2-\")\n```\n\n## On-chain Example\n\n- [/r/gov/dao/v3/memberstore:members?filter=T1](/r/gov/dao/v3/memberstore:members?filter=T1)\n\n## API\n\n```go\ntype Table struct {\n    Headings []string   // Column headers\n    Rows     [][]string // Table data rows\n}\n\n// `u`: Current URL for generating sort links\n// `table`: Table data structure\n// `paramPrefix`: Prefix for URL params (use for multiple tables)\nfunc Render(u *url.URL, table *Table, paramPrefix string) string\n```\n\n**URL Parameters:**\n- `{prefix}sort-asc={column}`: Sort column ascending\n- `{prefix}sort-desc={column}`: Sort column descending\n\n**URL Examples:**\n- `/users?sort-desc=Name` - Sort by Name descending\n- `/page?users-sort-asc=Age\u0026orders-sort-desc=Total` - Multiple tables\n\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/tablesort\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"},{"name":"render.gno","body":"package tablesort\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/mason/md\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Table holds the headings and rows for rendering.\n// Each row must have the same number of cells as there are headings.\ntype Table struct {\n\tHeadings []string   // [\"A\", \"B\", \"C\"]\n\tRows     [][]string // [[\"a1\",\"b1\",\"c1\"], [\"a2\",\"b2\",\"c2\"], ...]\n}\n\n// Render generates a Markdown table from a Table struct with sortable columns based on URL params.\n// paramPrefix is an optional prefix for in the URL to identify the tablesort Renders (e.g. \"members-\").\nfunc Render(u *url.URL, table *Table, paramPrefix string) string {\n\tdirection := \"\"\n\tcurrentHeading := \"\"\n\tif h := u.Query().Get(paramPrefix + \"sort-asc\"); h != \"\" {\n\t\tdirection = \"asc\"\n\t\tcurrentHeading = h\n\t} else if h := u.Query().Get(paramPrefix + \"sort-desc\"); h != \"\" {\n\t\tdirection = \"desc\"\n\t\tcurrentHeading = h\n\t}\n\n\tvar sb strings.Builder\n\n\t// Find the index of the column to sort\n\tcolIndex := -1\n\tfor i, h := range table.Headings {\n\t\tif h == currentHeading {\n\t\t\tcolIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Sort rows if necessary\n\tif colIndex != -1 {\n\t\tSortRows(table.Rows, colIndex, direction == \"asc\")\n\t}\n\n\t// Build header\n\tsb.WriteString(buildHeader(u, table.Headings, currentHeading, direction, paramPrefix))\n\tsb.WriteString(\"\\n\")\n\n\tnumCols := len(table.Headings)\n\n\t// Build rows\n\tfor i, row := range table.Rows {\n\t\t// Validate row length\n\t\tif len(row) != numCols {\n\t\t\treturn \"tablesort fails: row \" + ufmt.Sprintf(\"%d\", i+1) + \" has \" +\n\t\t\t\tufmt.Sprintf(\"%d\", len(row)) + \" cells, expected \" +\n\t\t\t\tufmt.Sprintf(\"%d\", numCols) + \", because there are \" + ufmt.Sprintf(\"%d\", numCols) + \" columns.\\n\"\n\t\t}\n\n\t\tsb.WriteString(\"|\")\n\t\tfor _, cell := range row {\n\t\t\tsb.WriteString(\" \" + cell + \" |\")\n\t\t}\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\treturn sb.String()\n}\n\n// buildHeader builds the Markdown header row with clickable links and arrows\nfunc buildHeader(u *url.URL, headings []string, currentHeading, direction string, paramPrefix string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"|\")\n\tfor _, h := range headings {\n\t\tarrow := \"\"\n\t\tif h == currentHeading {\n\t\t\tif direction == \"asc\" {\n\t\t\t\tarrow = \" ↑\"\n\t\t\t} else if direction == \"desc\" {\n\t\t\t\tarrow = \" ↓\"\n\t\t\t}\n\t\t}\n\n\t\t// Build URL for the header link with toggle logic\n\t\tnewURL := *u\n\t\tq := newURL.Query()\n\t\tif h == currentHeading {\n\t\t\t// Toggle sort direction\n\t\t\tif direction == \"asc\" {\n\t\t\t\tq.Del(paramPrefix + \"sort-asc\")\n\t\t\t\tq.Set(paramPrefix+\"sort-desc\", h)\n\t\t\t} else {\n\t\t\t\tq.Del(paramPrefix + \"sort-desc\")\n\t\t\t\tq.Set(paramPrefix+\"sort-asc\", h)\n\t\t\t}\n\t\t} else {\n\t\t\t// First click defaults to descending\n\t\t\tq.Del(paramPrefix + \"sort-asc\")\n\t\t\tq.Set(paramPrefix+\"sort-desc\", h)\n\t\t}\n\t\tnewURL.RawQuery = q.Encode()\n\t\tlink := md.Link(h+arrow, newURL.String())\n\t\tsb.WriteString(\" \" + link + \" |\")\n\t}\n\n\tsb.WriteString(\"\\n|\")\n\tfor range headings {\n\t\tsb.WriteString(\" --- |\")\n\t}\n\treturn sb.String()\n}\n"},{"name":"tablesort.gno","body":"// Package tablesort provides functionality to render a Markdown table with sortable columns.\n// It allows users to click on column headers to sort the table in ascending or descending sort direction.\n// The sorting state is managed via URL query parameters.\n// It displays an error if the table is malformed (e.g. rows with missing cells).\n// Multiple tablesort can be rendered on the same page by using a paramPrefix for each Render (See the Render function).\npackage tablesort\n\nimport (\n\t\"sort\"\n)\n\n// rowSorter implements sort.Interface for sorting rows by a specific column.\ntype rowSorter struct {\n\trows      [][]string\n\tcolIndex  int\n\tascending bool\n}\n\nfunc (rs rowSorter) Len() int {\n\treturn len(rs.rows)\n}\n\nfunc (rs rowSorter) Less(i, j int) bool {\n\tiCell := rs.rows[i][rs.colIndex]\n\tjCell := rs.rows[j][rs.colIndex]\n\tif rs.ascending {\n\t\treturn iCell \u003c jCell\n\t}\n\treturn iCell \u003e jCell\n}\n\nfunc (rs rowSorter) Swap(i, j int) {\n\trs.rows[i], rs.rows[j] = rs.rows[j], rs.rows[i]\n}\n\n// SortRows sorts the rows slice by a given column index and direction\nfunc SortRows(rows [][]string, colIndex int, ascending bool) {\n\tsort.Sort(rowSorter{rows, colIndex, ascending})\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"urlfilter","path":"gno.land/p/samcrew/urlfilter","files":[{"name":"README.md","body":"# `urlfilter` - URL-based filtering\n\nFilter items using URL query parameters with toggleable markdown links. Works with AVL tree structures where each filter contains its associated items.\n\nGiven filters `[\"T1\", \"T2\", \"size:XL\"]` and URL `/shop?filter=T1,size:XL`, it generates toggle links:\n\n- **T1** _(active, click to remove)_\n- ~~T2~~ _(inactive, click to add)_  \n- **size:XL** _(active, click to remove)_\n\n**Markdown output:**\n```markdown\n[**T1**](/p/samcrew/urlfilter?filter=size:XL) - [~~T2~~](/p/samcrew/urlfilter=T1,T2,size:XL) - [**size:XL**](/p/samcrew/urlfilter?filter=T1)\n```\n\n**Rendered as:**\n[**T1**](/p/samcrew/urlfilter?filter=size:XL) - [~~T2~~](/p/samcrew/urlfilter=T1,T2,size:XL) - [**size:XL**](/p/samcrew/urlfilter?filter=T1)\n\n## Usage\n\nThe package expects a two-level AVL tree structure:\n- **Top level**: Filter names as keys (e.g., \"T1\", \"size:XL\", \"on_sale\")  \n- **Second level**: Item trees containing the actual items for each filter\n\n```go\n// Build the main filters tree\nfilters := avl.NewTree()\n\n// Subtree for filter \"T1\" \nt1Items := avl.NewTree()\nt1Items.Set(\"key1\", \"item1\")\nt1Items.Set(\"key2\", \"item2\")\nfilters.Set(\"T1\", t1Items)\n\n// Subtree for filter \"size:XL\"\nt2Items := avl.NewTree()\nt2Items.Set(\"key3\", \"item3\")\nfilters.Set(\"T2\", t2Items)\n\n// URL with active filter \"T1\"\nu, _ := url.Parse(\"/shop?filter=T1\")\n\n// Apply filtering\nmdLinks, filteredItems := urlfilter.ApplyFilters(u, filters, \"filter\") // \"filter\" for /shop?*filter*=T1\n\n// mdLinks    → Markdown links for toggling filters  \n// filteredItems → AVL tree containing only filtered items\n```\n\n## API\n\n```go\nfunc ApplyFilters(u *url.URL, items *avl.Tree, paramName string) (string, *avl.Tree)\n```\n\n**Parameters:**\n- `u`: URL containing query parameters\n- `items`: Two-level AVL tree (filters → item trees)\n- `paramName`: Query parameter name (e.g., \"filter\" for /shop?filter=T1)\n\n**URL Format:**\n- Single filter: `?filter=T1`\n- Multiple filters: `?filter=T1,size:XL,on_sale`\n- Filter names are comma-separated\n\n**Returns:**\n- **Markdown links**: Toggleable filter links with formatting\n- **Filtered items**: AVL tree containing items from active filters\n  - If no filters active: returns all items\n  - Item keys are preserved, values show which filter matched\n\n# Example\n\n- [/r/gov/dao/v3/memberstore:members?filter=T1](/r/gov/dao/v3/memberstore:members?filter=T1)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/samcrew/urlfilter\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"},{"name":"urlfilter.gno","body":"// Package urlfilter provides functionality to filter items based on URL query parameters.\n// It is designed to work with an avl.Tree structure where each key represents a filter\n// and each value is an avl.Tree containing items associated with that filter.\npackage urlfilter\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sunspirit/md\"\n)\n\n// ApplyFilters filters items based on the \"filter\" query parameter in the given URL\n// and generates a Markdown representation of all available filters.\n//\n// Expected `items` structure:\n//   - `items` is an *bptree.BPTree where each key is a filter name (e.g., \"T1\", \"size:XL\", \"on_sale\")\n//     and each value is an *bptree.BPTree containing the items for that filter.\n//   - Each item tree uses:\n//     Key   (string): Unique item identifier\n//     Value (any)   : Optional associated item data\n//\n// Example:\n//\n//\t// Build the main filters tree\n//\tfilters := bptree.NewBPTree32()\n//\n//\t// Subtree for filter \"T1\"\n//\tt1Items := bptree.NewBPTree32()\n//\tt1Items.Set(\"item1\", nil)\n//\tt1Items.Set(\"item2\", nil)\n//\tfilters.Set(\"T1\", t1Items)\n//\n//\t// URL with active filter \"T1\"\n//\tu, _ := url.Parse(\"/shop?filter=T1\")\n//\n//\tmdFilters, items := ApplyFilters(u, filters, \"filter\")\n//\n//\t// mdFilters\t→ Markdown links for toggling filters\n//\t// items    \t→ AVL tree containing the filtered items\nfunc ApplyFilters(u *url.URL, items *bptree.BPTree, paramName string) (string, *bptree.BPTree) {\n\tactive := parseFilterMap(u.Query(), paramName)\n\tallFilters := make([]string, 0)\n\tresultTree := bptree.NewBPTree32()\n\n\t// Iterate over each filter group in the items tree\n\titems.Iterate(\"\", \"\", func(filterKey string, subtree interface{}) bool {\n\t\tallFilters = append(allFilters, filterKey)\n\n\t\ttree, ok := subtree.(*bptree.BPTree)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\t// Add items to result if there are no active filters\n\t\t// or if the current filter is active\n\t\ttree.Iterate(\"\", \"\", func(itemKey string, _ interface{}) bool {\n\t\t\tif len(active) == 0 || active[filterKey] {\n\t\t\t\tresultTree.Set(itemKey, filterKey)\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t\treturn false\n\t})\n\n\t// Build Markdown links for toggling each filter\n\tvar sb strings.Builder\n\tfor _, f := range allFilters {\n\t\tq := toggleFilterQuery(active, f, allFilters, paramName)\n\t\turlStr := buildURL(u.Path, q)\n\t\tsb.WriteString(ufmt.Sprintf(\" | %v \", md.Link(formatLabel(f, active[f]), urlStr)))\n\t}\n\n\treturn sb.String(), resultTree\n}\n\n// buildURL returns a path + query string, omitting the \"?\" if no query exists.\nfunc buildURL(path string, query url.Values) string {\n\tif enc := query.Encode(); enc != \"\" {\n\t\treturn path + \"?\" + enc\n\t}\n\treturn path\n}\n\n// parseFilterMap reads the \"filter\" query parameter and converts it into a map\n// where keys are filter names and values are true for active filters.\n//\n// Example:\n//\n//\t\"filter=T1,T2\" -\u003e map[string]bool{\"T1\": true, \"T2\": true}\nfunc parseFilterMap(query url.Values, paramName string) map[string]bool {\n\tfilterStr := strings.TrimSpace(query.Get(paramName))\n\tif filterStr == \"\" {\n\t\treturn map[string]bool{}\n\t}\n\tm := make(map[string]bool)\n\tfor _, f := range strings.Split(filterStr, \",\") {\n\t\tif f = strings.TrimSpace(f); f != \"\" {\n\t\t\tm[f] = true\n\t\t}\n\t}\n\treturn m\n}\n\n// toggleFilterQuery returns a new query string with the given filter toggled.\n// - If the filter is currently active, it will be removed.\n// - If it is inactive, it will be added.\n// The order of filters follows the `all` list for consistency.\nfunc toggleFilterQuery(active map[string]bool, toggled string, all []string, paramName string) url.Values {\n\tnewFilters := []string{}\n\tfor _, f := range all {\n\t\tif f == toggled {\n\t\t\tif !active[f] { // Add if it was inactive\n\t\t\t\tnewFilters = append(newFilters, f)\n\t\t\t}\n\t\t} else if active[f] { // Keep other active filters\n\t\t\tnewFilters = append(newFilters, f)\n\t\t}\n\t}\n\tq := url.Values{}\n\tif len(newFilters) \u003e 0 {\n\t\tq.Set(paramName, strings.Join(newFilters, \",\"))\n\t}\n\treturn q\n}\n\n// formatLabel returns the Markdown-formatted label for a filter,\n// showing active filters in bold (**filter**) and inactive filters\n// with strikethrough (~~filter~~).\nfunc formatLabel(name string, active bool) string {\n\tif active {\n\t\treturn md.Bold(name)\n\t}\n\treturn md.Strikethrough(name)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"memberstore","path":"gno.land/r/gov/dao/v3/memberstore","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/v3/memberstore\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"},{"name":"memberstore.gno","body":"package memberstore\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\nvar (\n\tmembers MembersByTier\n\ttiers   TiersByName // private to prevent external modification\n\trouter  *mux.Router\n)\n\nconst (\n\tT1 = \"T1\"\n\tT2 = \"T2\"\n\tT3 = \"T3\"\n)\n\nfunc init() {\n\tmembers = NewMembersByTier()\n\n\ttiers = TiersByName{bptree.NewBPTree32()}\n\ttiers.Set(T1, Tier{\n\t\tInvitationPoints: 3,\n\t\tMinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 70\n\t\t},\n\t\tMaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 0\n\t\t},\n\t\tBasePower: 3,\n\t\tPowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {\n\t\t\treturn 3\n\t\t},\n\t})\n\n\ttiers.Set(T2, Tier{\n\t\tInvitationPoints: 2,\n\t\tMaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn membersByTier.GetTierSize(T1) * 2\n\t\t},\n\t\tMinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn membersByTier.GetTierSize(T1) / 4\n\t\t},\n\t\tBasePower: 2,\n\t\tPowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {\n\t\t\tt1ms := float64(membersByTier.GetTierSize(T1))\n\t\t\tt1, _ := tiersByName.GetTier(T1)\n\t\t\tt2ms := float64(membersByTier.GetTierSize(T2))\n\t\t\tt2, _ := tiersByName.GetTier(T2)\n\n\t\t\tt1p := t1.BasePower * t1ms\n\t\t\tt2p := t2.BasePower * t2ms\n\n\t\t\t// capped to 2/3 of tier 1\n\t\t\tt1ptreshold := t1p * (2.0 / 3.0)\n\t\t\tif t2p \u003e t1ptreshold {\n\t\t\t\treturn t1ptreshold / t2ms\n\t\t\t}\n\n\t\t\treturn t2.BasePower\n\t\t},\n\t})\n\n\ttiers.Set(T3, Tier{\n\t\tInvitationPoints: 1,\n\t\tMaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 0\n\t\t},\n\t\tMinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 0\n\t\t},\n\t\tBasePower: 1,\n\t\tPowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {\n\t\t\tt1ms := float64(membersByTier.GetTierSize(T1))\n\t\t\tt1, _ := tiersByName.GetTier(T1)\n\t\t\tt3ms := float64(membersByTier.GetTierSize(T3))\n\t\t\tt3, _ := tiersByName.GetTier(T3)\n\n\t\t\tt1p := t1.BasePower * t1ms\n\t\t\tt3p := t3.BasePower * t3ms\n\n\t\t\t// capped to 1/3 of tier 1\n\t\t\tt1ptreshold := t1p * (1.0 / 3.0)\n\t\t\tif t3p \u003e t1ptreshold {\n\t\t\t\treturn t1ptreshold / t3ms\n\t\t\t}\n\n\t\t\treturn t3.BasePower\n\t\t},\n\t})\n\n\tinitRouter()\n}\n\n// initRouter initializes the router for the memberstore.\nfunc initRouter() {\n\trouter = mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHome)\n\trouter.HandleFunc(\"members\", renderMembers)\n\trouter.NotFoundHandler = renderNotFound\n}\n\n// renderHome displays the tiers data (Number of members and powers) and tiers charts.\nfunc renderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tvar sb strings.Builder\n\tsb.WriteString(md.Link(\"\u003e Go to Members list \u003c\", \"/r/gov/dao/v3/memberstore:members\") + \"\\n\")\n\n\tmembers.Iterate(\"\", \"\", func(tn string, ti interface{}) bool {\n\t\ttree, ok := ti.(*bptree.BPTree)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\ttier, ok := tiers.GetTier(tn)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\ttp := (tier.PowerHandler(members, tiers) * float64(members.GetTierSize(tn)))\n\n\t\tsb.WriteString(ufmt.Sprintf(\"- %v Tier %v contains %v members with power: %v\\n\", tierColoredChip(tn), tn, tree.Size(), tp))\n\n\t\treturn false\n\t})\n\n\tsb.WriteString(\"\\n\" + RenderCharts(members))\n\tres.Write(sb.String())\n}\n\n// renderMembers displays the members list.\nfunc renderMembers(res *mux.ResponseWriter, req *mux.Request) {\n\tpath := strings.Replace(req.RawPath, \"members\", \"\", 1) // We have to clean the path\n\tres.Write(RenderMembers(path, members))\n}\n\nfunc renderNotFound(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(\"# 404\\n\\nThat page was not found. Would you like to [**go home**?](/r/gov/dao/v3/memberstore)\")\n}\n\nfunc tierColor(tn string) string {\n\tswitch tn {\n\tcase T1:\n\t\treturn \"#329175\"\n\tcase T2:\n\t\treturn \"#21577A\"\n\tcase T3:\n\t\treturn \"#F3D3BC\"\n\tdefault:\n\t\treturn \"#FFF\"\n\t}\n}\n\n// tierColoredChip returns a colored chip svg for the given tier name.\nfunc tierColoredChip(tn string) string {\n\tcanvas := svg.NewCanvas(16, 16)\n\tcanvas.Append(svg.NewRectangle(0, 0, 16, 16, tierColor(tn)))\n\treturn canvas.Render(tn + \" colored chip\")\n}\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(md.H1(\"Memberstore Govdao v3\"))\n\tsb.WriteString(router.Render(path))\n\treturn sb.String()\n}\n\n// Get gets the Members store.\n//\n// rlm is the cur of an in-scope crossing frame, threaded by the caller.\n// The IsCurrent() check rejects stale or stashed realm values — a\n// malicious realm cannot replay an old cur to claim allowed-DAO\n// identity. After the check, rlm.PkgPath() is the authentic immediate\n// caller's realm.\nfunc Get(_ int, rlm realm) MembersByTier {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"memberstore.Get: rlm is not the caller's live cur (stale capture or sibling frame)\")\n\t}\n\tcurrealm := rlm.PkgPath()\n\tif !dao.InAllowedDAOs(currealm) {\n\t\tpanic(\"this Realm is not allowed to get the Members data: \" + currealm)\n\t}\n\n\treturn members\n}\n\n// GetTier returns a tier by name. This is a read-only accessor.\nfunc GetTier(name string) (Tier, bool) {\n\treturn tiers.GetTier(name)\n}\n\n// IterateTiers iterates over all tiers in order. This is a read-only accessor.\n// The callback receives the tier name and tier data.\n// Return true from the callback to stop iteration.\nfunc IterateTiers(fn func(name string, tier Tier) bool) {\n\ttiers.Iterate(\"\", \"\", func(name string, value interface{}) bool {\n\t\ttier, ok := value.(Tier)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\t\treturn fn(name, tier)\n\t})\n}\n\n// setTiers replaces the tiers configuration.\n// This is internal and should only be called via governance proposal execution.\nfunc setTiers(newTiers TiersByName) {\n\ttiers = newTiers\n}\n\n// GetTierPower calculates the effective voting power for a tier given the current members.\n// This is a safe accessor that uses the internal tiers configuration.\nfunc GetTierPower(tierName string, members MembersByTier) float64 {\n\ttier, ok := tiers.GetTier(tierName)\n\tif !ok {\n\t\treturn 0\n\t}\n\treturn tier.PowerHandler(members, tiers)\n}\n"},{"name":"prop_requests.gno","body":"package memberstore\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\nfunc NewChangeTiersRequest(cur realm, tiers map[string]Tier) dao.ProposalRequest {\n\tif len(tiers) == 0 {\n\t\tpanic(\"tiers list is empty\")\n\t}\n\n\tmember, _ := Get(0, cur).GetMember(unsafe.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tnewTiers := TiersByName{bptree.NewBPTree32()}\n\tfor name, tier := range tiers {\n\t\tnewTiers.Set(name, tier)\n\t}\n\n\tcallback := func(cur realm) error {\n\t\tsetTiers(newTiers)\n\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, callback, \"New set of tiers proposed.\")\n\n\treturn dao.NewProposalRequest(\"Change Tiers Proposal\", \"This proposal is looking to change the existing Tiers in memberstore\", e)\n}\n"},{"name":"rendercharts.gno","body":"package memberstore\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/samcrew/piechart\"\n)\n\n// RenderCharts generates two pie charts for member tiers:\n// 1) distribution of member counts per tier\n// 2) distribution of power per tier\nfunc RenderCharts(members MembersByTier) string {\n\tvar sb strings.Builder\n\n\ttierNames := []string{T1, T2, T3}\n\tpieSlicesTs := make([]piechart.PieSlice, 0, len(tierNames))\n\tpieSlicesTp := make([]piechart.PieSlice, 0, len(tierNames))\n\n\tfor _, tn := range tierNames {\n\t\ttier, ok := tiers.GetTier(tn)\n\t\tif !ok {\n\t\t\treturn \"\"\n\t\t}\n\n\t\tts := float64(members.GetTierSize(tn))\n\t\ttp := tier.PowerHandler(members, tiers) * ts\n\n\t\tpieSlicesTs = append(pieSlicesTs, piechart.PieSlice{\n\t\t\tValue: ts,\n\t\t\tColor: tierColor(tn),\n\t\t\tLabel: tn,\n\t\t})\n\t\tpieSlicesTp = append(pieSlicesTp, piechart.PieSlice{\n\t\t\tValue: tp,\n\t\t\tColor: tierColor(tn),\n\t\t\tLabel: tn,\n\t\t})\n\t}\n\n\t// Render pie charts for members count and power distribution\n\tresultPieChartTs := piechart.Render(pieSlicesTs, \"Members distribution:\")\n\tresultPieChartTp := piechart.Render(pieSlicesTp, \"Power distribution:\")\n\n\tsb.WriteString(resultPieChartTs + \"\\n\")\n\tsb.WriteString(resultPieChartTp + \"\\n\")\n\n\treturn sb.String()\n}\n"},{"name":"rendermembers.gno","body":"package memberstore\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/bptree/v0/pager\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/samcrew/tablesort\"\n\t\"gno.land/p/samcrew/urlfilter\"\n)\n\n// RenderMembers returns the members list with tier filters and pagination.\nfunc RenderMembers(path string, members MembersByTier) string {\n\tu, _ := url.Parse(path)\n\tmdFilters, items := urlfilter.ApplyFilters(u, members.BPTree, \"filter\")\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.Link(\"\u003e Go to Tiers summary \u003c\", \"/r/gov/dao/v3/memberstore\") + \"\\n\\n\")\n\tsb.WriteString(md.Bold(\"Filter members by tiers:\"))\n\tsb.WriteString(mdFilters + \"\\n\")\n\n\tconst pageSize = 14\n\tpager := pager.NewPager(items, pageSize, false)\n\tpage := pager.MustGetPageByPath(path)\n\n\tsb.WriteString(renderMembersPages(u, page, items) + \"\\n\")\n\tsb.WriteString(renderPagination(u, page))\n\n\treturn sb.String()\n}\n\n// renderMembersPages returns the members of each page.\nfunc renderMembersPages(u *url.URL, page *pager.Page, members *bptree.BPTree) string {\n\tvar sb strings.Builder\n\n\ttable := \u0026tablesort.Table{\n\t\tHeadings: []string{\"Tier\", \"Address\"},\n\t\tRows:     [][]string{},\n\t}\n\n\tfor _, item := range page.Items {\n\t\taddr := item.Key\n\t\ttn := members.Get(addr)\n\t\ttnStr, _ := tn.(string)\n\t\ttierCell := ufmt.Sprintf(\"%s %s\", tierColoredChip(tnStr), tn)\n\t\ttable.Rows = append(table.Rows, []string{tierCell, addr})\n\t}\n\n\tsb.WriteString(tablesort.Render(u, table, \"\"))\n\n\treturn sb.String()\n}\n\n// renderPagination returns the pagination UI for the current page.\nfunc renderPagination(u *url.URL, page *pager.Page) string {\n\tq := u.Query()\n\tq.Del(\"page\")\n\tu.RawQuery = q.Encode()\n\n\tvar sb strings.Builder\n\tsb.WriteString(page.Picker(u.String()))\n\n\treturn sb.String()\n}\n"},{"name":"types.gno","body":"package memberstore\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\ntype ErrMemberAlreadyExists struct {\n\tTier string\n}\n\nfunc (e *ErrMemberAlreadyExists) Error() string {\n\treturn \"member already exists on tier \" + e.Tier\n}\n\ntype Member struct {\n\tInvitationPoints int\n}\n\nfunc NewMember(invitationPoints int) *Member {\n\treturn \u0026Member{InvitationPoints: invitationPoints}\n}\n\nfunc (m *Member) RemoveInvitationPoint() {\n\tif m.InvitationPoints \u003c= 0 {\n\t\tpanic(\"not enough invitation points\")\n\t}\n\n\tm.InvitationPoints = m.InvitationPoints - 1\n}\n\n// MembersByTier contains all `Member`s indexed by their Address.\ntype MembersByTier struct {\n\t*bptree.BPTree // tier name -\u003e address -\u003e member\n}\n\nfunc NewMembersByTier() MembersByTier {\n\treturn MembersByTier{BPTree: bptree.NewBPTree32()}\n}\n\nfunc (mbt MembersByTier) DeleteAll() {\n\tmbt.Iterate(\"\", \"\", func(tn string, msv interface{}) bool {\n\t\tmbt.Remove(tn)\n\t\treturn false\n\t})\n}\n\nfunc (mbt MembersByTier) SetTier(tier string) error {\n\tif ok := mbt.Has(tier); ok {\n\t\treturn errors.New(\"tier already exist: \" + tier)\n\t}\n\n\tmbt.Set(tier, bptree.NewBPTree32())\n\n\treturn nil\n}\n\n// GetTierSize tries to get how many members are on the specified tier. If the tier does not exists, it returns 0.\nfunc (mbt MembersByTier) GetTierSize(tn string) int {\n\ttv := mbt.Get(tn)\n\tif tv == nil {\n\t\treturn 0\n\t}\n\n\ttree, ok := tv.(*bptree.BPTree)\n\tif !ok {\n\t\treturn 0\n\t}\n\n\treturn tree.Size()\n}\n\n// SetMember adds a new member to the specified tier. The tier index is created on the fly if it does not exists.\nfunc (mbt MembersByTier) SetMember(tier string, addr address, member *Member) error {\n\t_, t := mbt.GetMember(addr)\n\tif t != \"\" {\n\t\treturn \u0026ErrMemberAlreadyExists{Tier: t}\n\t}\n\n\tif ok := mbt.Has(tier); !ok {\n\t\treturn errors.New(\"tier does not exist: \" + tier)\n\t}\n\n\tms := mbt.Get(tier)\n\tmst := ms.(*bptree.BPTree)\n\n\tmst.Set(string(addr), member)\n\n\treturn nil\n}\n\n// GetMember iterate over all tiers to try to find a member by its address. The tier ID is also returned if the Member is found.\nfunc (mbt MembersByTier) GetMember(addr address) (m *Member, t string) {\n\tmbt.Iterate(\"\", \"\", func(tn string, msv interface{}) bool {\n\t\tmst, ok := msv.(*bptree.BPTree)\n\t\tif !ok {\n\t\t\tpanic(\"MembersByTier values can only be bptree.BPTree\")\n\t\t}\n\n\t\tmv := mst.Get(string(addr))\n\t\tif mv == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tmm, ok := mv.(*Member)\n\t\tif !ok {\n\t\t\tpanic(\"MembersByTier values can only be *Member\")\n\t\t}\n\n\t\tm = mm\n\t\tt = tn\n\n\t\treturn true\n\t})\n\n\treturn\n}\n\n// RemoveMember removes a member from any tier\nfunc (mbt MembersByTier) RemoveMember(addr address) (t string) {\n\tmbt.Iterate(\"\", \"\", func(tn string, msv interface{}) bool {\n\t\tmst, ok := msv.(*bptree.BPTree)\n\t\tif !ok {\n\t\t\tpanic(\"MembersByTier values can only be bptree.BPTree\")\n\t\t}\n\n\t\t_, removed := mst.Remove(string(addr))\n\t\tif removed {\n\t\t\tt = tn\n\t\t}\n\t\treturn removed\n\t})\n\n\treturn\n}\n\n// GetTotalPower obtains the total voting power from all the specified tiers.\nfunc (mbt MembersByTier) GetTotalPower() float64 {\n\tvar out float64\n\tmbt.Iterate(\"\", \"\", func(tn string, msv interface{}) bool {\n\t\ttier, ok := tiers.GetTier(tn)\n\t\tif !ok {\n\t\t\t// tier does not exists, so we cannot count power from this tier\n\t\t\treturn false\n\t\t}\n\n\t\tout = out + (tier.PowerHandler(mbt, tiers) * float64(mbt.GetTierSize(tn)))\n\n\t\treturn false\n\t})\n\n\treturn out\n}\n\ntype Tier struct {\n\t// BasePower defines the standard voting power for the members on this tier.\n\tBasePower float64\n\n\t// InvitationPoints defines how many invitation points users on that tier will receive.\n\tInvitationPoints int\n\n\t// MaxSize calculates the max amount of members expected to be on this tier.\n\tMaxSize func(membersByTier MembersByTier, tiersByName TiersByName) int\n\n\t// MinSize calculates the min amount of members expected to be on this tier.\n\tMinSize func(membersByTier MembersByTier, tiersByName TiersByName) int\n\n\t// PowerHandler calculates what is the final power of this tier after taking into account Members by other tiers.\n\tPowerHandler func(membersByTier MembersByTier, tiersByName TiersByName) float64\n}\n\n// TiersByName contains all tier objects indexed by its name.\ntype TiersByName struct {\n\t*bptree.BPTree // *bptree.BPTree[string]Tier\n}\n\n// GetTier obtains a Tier struct by its name. It returns false if the Tier is not found.\nfunc (tbn TiersByName) GetTier(tn string) (Tier, bool) {\n\tval := tbn.Get(tn)\n\tif val == nil {\n\t\treturn Tier{}, false\n\t}\n\n\tt, ok := val.(Tier)\n\tif !ok {\n\t\tpanic(\"TiersByName must contains only Tier types\")\n\t}\n\n\treturn t, true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"treasury","path":"gno.land/r/gov/dao/v3/treasury","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/v3/treasury\"\ngno = \"0.9\"\n"},{"name":"treasury.gno","body":"package treasury\n\nimport (\n\t\"chain/banker\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\tt \"gno.land/p/nt/treasury/v0\"\n\n\t\"gno.land/r/demo/defi/grc20reg\"\n\t\"gno.land/r/gov/dao\"\n)\n\nvar (\n\ttreasury  *t.Treasury\n\ttokenKeys = []string{\n\t\t// TODO: Add the default GRC20 tokens we want to support here.\n\t}\n)\n\nfunc init(cur realm) {\n\t// Define a token lister for the GRC20Banker.\n\t// For now, GovDAO uses a static list of tokens.\n\tgrc20Lister := func() map[string]*grc20.Token {\n\t\t// Get the GRC20 tokens from the registry.\n\t\ttokens := map[string]*grc20.Token{}\n\t\tfor _, key := range tokenKeys {\n\t\t\t// Get the token by its key.\n\t\t\ttoken := grc20reg.Get(key)\n\t\t\tif token != nil {\n\t\t\t\ttokens[key] = token\n\t\t\t}\n\t\t}\n\n\t\treturn tokens\n\t}\n\n\t// Init the treasury bankers.\n\tcoinsBanker, err := t.NewCoinsBankerWithOwner(cur.Address(), banker.NewBanker(banker.BankerTypeRealmSend, cur))\n\tif err != nil {\n\t\tpanic(\"failed to create CoinsBanker: \" + err.Error())\n\t}\n\tgrc20Banker, err := t.NewGRC20BankerWithOwner(cur.Address(), grc20Lister)\n\tif err != nil {\n\t\tpanic(\"failed to create GRC20Banker: \" + err.Error())\n\t}\n\tbankers := []t.Banker{\n\t\tcoinsBanker,\n\t\tgrc20Banker,\n\t}\n\n\t// Create the treasury instance with the bankers. cur.PkgPath() is\n\t// captured for render-link construction (See full history → /r/gov/dao/v3/treasury:.../history).\n\ttreasury, err = t.New(bankers, cur.PkgPath())\n\tif err != nil {\n\t\tpanic(\"failed to create treasury: \" + err.Error())\n\t}\n}\n\n// SetTokenKeys sets the GRC20 token registry keys that the treasury will use.\nfunc SetTokenKeys(cur realm, keys []string) {\n\tcaller := cur.Previous().PkgPath()\n\n\t// Check if the caller realm is allowed to set token keys.\n\tif !dao.InAllowedDAOs(caller) {\n\t\tpanic(\"this Realm is not allowed to send payment: \" + caller)\n\t}\n\n\ttokenKeys = keys\n}\n\n// Send sends a payment using the treasury instance.\nfunc Send(cur realm, payment t.Payment) {\n\tcaller := cur.Previous().PkgPath()\n\n\t// Check if the caller realm is allowed to send payments.\n\tif !dao.InAllowedDAOs(caller) {\n\t\tpanic(\"this Realm is not allowed to send payment: \" + caller)\n\t}\n\n\t// Send the payment using the treasury instance. cur is this realm's\n\t// captured cur — passes IsCurrent inside Banker.Send and matches the\n\t// banker's owner (this realm's address) registered at init.\n\tif err := treasury.Send(0, cur, payment); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// History returns the payment history sent by the banker with the given ID.\n// Payments are paginated, with the most recent payments first.\nfunc History(bankerID string, pageNumber int, pageSize int) []t.Payment {\n\thistory, err := treasury.History(bankerID, pageNumber, pageSize)\n\tif err != nil {\n\t\tpanic(\"failed to get history: \" + err.Error())\n\t}\n\n\treturn history\n}\n\n// Balances returns the balances of the banker with the given ID.\nfunc Balances(bankerID string) []t.Balance {\n\tbalances, err := treasury.Balances(bankerID)\n\tif err != nil {\n\t\tpanic(\"failed to get balances: \" + err.Error())\n\t}\n\n\treturn balances\n}\n\n// Address returns the address of the banker with the given ID.\nfunc Address(bankerID string) string {\n\taddr, err := treasury.Address(bankerID)\n\tif err != nil {\n\t\tpanic(\"failed to get address: \" + err.Error())\n\t}\n\n\treturn addr\n}\n\n// HasBanker checks if a banker with the given ID is registered.\nfunc HasBanker(bankerID string) bool {\n\treturn treasury.HasBanker(bankerID)\n}\n\n// ListBankerIDs returns a list of all registered banker IDs.\nfunc ListBankerIDs() []string {\n\treturn treasury.ListBankerIDs()\n}\n\nfunc Render(path string) string {\n\treturn treasury.Render(path)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"impl","path":"gno.land/r/gov/dao/v3/impl","files":[{"name":"filter.gno","body":"package impl\n\ntype FilterByTier struct {\n\tTier string\n}\n\nfunc NewFilterByTier(tier string) FilterByTier {\n\treturn FilterByTier{Tier: tier}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/v3/impl\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"},{"name":"govdao.gno","body":"package impl\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nvar ErrMemberNotFound = errors.New(\"member not found\")\n\ntype GovDAO struct {\n\tpss    ProposalsStatuses\n\trender *render\n}\n\nfunc NewGovDAO() *GovDAO {\n\tpss := NewProposalsStatuses()\n\td := \u0026GovDAO{\n\t\tpss: pss,\n\t}\n\n\td.render = NewRender(d)\n\n\t// Attach to package var (impl owns _govdao). Plain assignment is\n\t// fine — we're in impl's package, no realm transition needed.\n\t// TODO: replace with future attach().\n\t_govdao = d\n\n\treturn d\n}\n\n// Setting this to a global variable forces attaching the GovDAO struct to this\n// realm. TODO replace with future `attach()`.\nvar _govdao *GovDAO\n\nfunc (g *GovDAO) PreCreateProposal(_ int, rlm realm, r dao.ProposalRequest) (address, error) {\n\tif !g.isValidCall(0, rlm) {\n\t\treturn \"\", errors.New(ufmt.Sprintf(\"proposal creation must be done directly by a user or through the r/gov/dao proxy. caller realm: %v; caller's previous: %v\",\n\t\t\trlm, rlm.Previous()))\n\t}\n\n\t// Verify that the one creating the proposal is a member.\n\tcaller := unsafe.OriginCaller()\n\tmem, _ := getMembers(cross(rlm)).GetMember(caller)\n\tif mem == nil {\n\t\treturn caller, errors.New(\"only members can create new proposals\")\n\t}\n\n\treturn caller, nil\n}\n\nfunc (g *GovDAO) PostCreateProposal(_ int, rlm realm, r dao.ProposalRequest, pid dao.ProposalID) {\n\t// Tiers Allowed to Vote\n\ttatv := []string{memberstore.T1, memberstore.T2, memberstore.T3}\n\tswitch v := r.Filter().(type) {\n\tcase FilterByTier:\n\t\t// only members from T1 are allowed to vote when adding new members to T1\n\t\tif v.Tier == memberstore.T1 {\n\t\t\ttatv = []string{memberstore.T1}\n\t\t}\n\t\t// only members from T1 and T2 are allowed to vote when adding new members to T2\n\t\tif v.Tier == memberstore.T2 {\n\t\t\ttatv = []string{memberstore.T1, memberstore.T2}\n\t\t}\n\t}\n\tg.pss.Set(pid.String(), newProposalStatus(tatv))\n}\n\nfunc (g *GovDAO) VoteOnProposal(_ int, rlm realm, r dao.VoteRequest) error {\n\tif !g.isValidCall(0, rlm) {\n\t\treturn errors.New(\"proposal voting must be done directly by a user\")\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\tmem, tie := getMembers(cross(rlm)).GetMember(caller)\n\tif mem == nil {\n\t\treturn ErrMemberNotFound\n\t}\n\n\tstatus := g.pss.GetStatus(r.ProposalID)\n\tif status == nil {\n\t\treturn errors.New(\"proposal not found\")\n\t}\n\n\tif status.Denied || status.Accepted {\n\t\treturn errors.New(ufmt.Sprintf(\"proposal closed. Accepted: %v\", status.Accepted))\n\t}\n\n\tif !status.IsAllowed(tie) {\n\t\treturn errors.New(\"member on specified tier is not allowed to vote on this proposal\")\n\t}\n\n\tmVoted, _ := status.AllVotes.GetMember(caller)\n\tif mVoted != nil {\n\t\treturn errors.New(\"already voted on proposal\")\n\t}\n\n\tswitch r.Option {\n\tcase dao.YesVote:\n\t\tstatus.AllVotes.SetMember(tie, caller, mem)\n\t\tstatus.YesVotes.SetMember(tie, caller, mem)\n\tcase dao.NoVote:\n\t\tstatus.AllVotes.SetMember(tie, caller, mem)\n\t\tstatus.NoVotes.SetMember(tie, caller, mem)\n\tcase dao.AbstainVote:\n\t\tstatus.AllVotes.SetMember(tie, caller, mem)\n\t\tstatus.AbstainVotes.SetMember(tie, caller, mem)\n\tdefault:\n\t\treturn errors.New(\"voting can only be YES, NO, or ABSTAIN\")\n\t}\n\n\treturn nil\n}\n\nfunc (g *GovDAO) PreExecuteProposal(_ int, rlm realm, pid dao.ProposalID) (bool, error) {\n\tif !g.isValidCall(0, rlm) {\n\t\treturn false, errors.New(\"proposal execution must be done directly by a user\")\n\t}\n\tstatus := g.pss.GetStatus(pid)\n\tif status.Denied || status.Accepted {\n\t\treturn false, errors.New(ufmt.Sprintf(\"proposal already executed. Accepted: %v\", status.Accepted))\n\t}\n\n\tif status.YesPercent(0, rlm) \u003e= law.Supermajority {\n\t\tstatus.Accepted = true\n\t\treturn true, nil\n\t}\n\n\tif status.NoPercent(0, rlm) \u003e= law.Supermajority {\n\t\tstatus.Denied = true\n\t\treturn false, nil\n\t}\n\n\treturn false, errors.New(ufmt.Sprintf(\"proposal didn't reach supermajority yet: %v\", law.Supermajority))\n}\n\nfunc (g *GovDAO) ExecuteProposal(_ int, rlm realm, pid dao.ProposalID, e dao.Executor) error {\n\tif e == nil {\n\t\tpanic(\"an executor is required to execute the proposal\")\n\t}\n\n\tstatus := g.pss.GetStatus(pid)\n\tif status == nil {\n\t\tpanic(\"proposal not found\")\n\t}\n\n\terr := e.Execute(cross(rlm))\n\tif err != nil {\n\t\tstatus.Accepted = false\n\t\tstatus.Denied = true\n\t\tstatus.DeniedReason = \"execution failed: \" + err.Error()\n\t}\n\treturn err\n}\n\nfunc (g *GovDAO) Render(cur realm, pkgPath string, path string) string {\n\t// Same-realm dispatch: pass cur through as data (non-crossing).\n\treturn g.render.Render(0, cur, pkgPath, path)\n}\n\n// isValidCall verifies that the impl method is being invoked from the\n// r/gov/dao proxy via a legitimate user transaction (MsgCall or MsgRun).\n//\n// The proxy passes its own crossing-frame Cur as rlm when calling the\n// impl methods. rlm.IsCurrent() rejects stale or stashed realm values —\n// a malicious realm cannot replay a captured proxy cur to impersonate\n// the proxy. After the IsCurrent() check:\n//   - rlm.PkgPath() == \"gno.land/r/gov/dao\" identifies the proxy\n//     unforgeably (pkg path is set at mint time by installCrossingCur).\n//   - rlm.Previous() is the caller of the proxy.\n//\n// The proxy is the only legitimate entrypoint. The impl methods are\n// non-crossing and take rlm as a regular argument, so a direct user\n// MsgCall to them cannot supply a valid rlm: the IsCurrent() check\n// rejects any forged or stashed realm value.\nfunc (g *GovDAO) isValidCall(_ int, rlm realm) bool {\n\tif !rlm.IsCurrent() {\n\t\treturn false\n\t}\n\tif rlm.PkgPath() != \"gno.land/r/gov/dao\" {\n\t\treturn false\n\t}\n\tprev := rlm.Previous()\n\t// MsgCall: proxy was called directly by an EOA (UserRealm).\n\tif prev.IsUser() {\n\t\treturn true\n\t}\n\t// MsgRun: proxy was called from the ephemeral run realm; that\n\t// realm's package address equals the EOA OriginCaller.\n\treturn chain.PackageAddress(prev.PkgPath()) == unsafe.OriginCaller()\n}\n"},{"name":"impl.gno","body":"package impl\n\nimport (\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nvar (\n\tlaw    *Law\n\tgovDAO *GovDAO = NewGovDAO()\n)\n\nfunc init() {\n\tlaw = \u0026Law{\n\t\tSupermajority: 66.66, // Two thirds\n\t}\n}\n\nfunc Render(cur realm, in string) string {\n\t// Same-realm: pass cur to govDAO.Render (also crossing, but same realm\n\t// so use the literal cur form rather than cross(cur)).\n\treturn govDAO.Render(cur, cur.PkgPath(), in)\n}\n\n// AddMember allows T1 and T2 members to freely add T3 members using their invitation points.\nfunc AddMember(cur realm, addr address) {\n\tcaller := cur.Previous()\n\tif !caller.IsUser() {\n\t\tpanic(\"this function must be called by an EOA through msg call or msg run\")\n\t}\n\tm, t := memberstore.Get(0, cur).GetMember(caller.Address())\n\tif m == nil {\n\t\tpanic(\"caller is not a member\")\n\t}\n\n\tif t != memberstore.T1 \u0026\u0026 t != memberstore.T2 {\n\t\tpanic(\"caller is not on T1 or T2. To add members, propose them through proposals\")\n\t}\n\n\tm.RemoveInvitationPoint()\n\n\tif err := memberstore.Get(0, cur).SetMember(memberstore.T3, addr, memberByTier(memberstore.T3)); err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\n// GetInstance returns the singleton *GovDAO. Only the loader realm may\n// call it (used during the bootstrap UpdateImpl handoff). The\n// IsCurrent() check rejects stale or stashed realm values; PkgPath()\n// after the check is the authentic immediate caller.\nfunc GetInstance(_ int, rlm realm) *GovDAO {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"GetInstance: rlm is not the caller's live cur (stale capture or sibling frame)\")\n\t}\n\tif rlm.PkgPath() != \"gno.land/r/gov/dao/v3/loader\" {\n\t\tpanic(\"not allowed\")\n\t}\n\n\treturn govDAO\n}\n"},{"name":"prop_requests.gno","body":"package impl\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/aeddi/panictoerr\"\n\t\"gno.land/p/moul/md\"\n\ttrs_pkg \"gno.land/p/nt/treasury/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n\t\"gno.land/r/gov/dao/v3/treasury\"\n)\n\nfunc NewChangeLawRequest(cur realm, newLaw Law) dao.ProposalRequest {\n\tmember, _ := memberstore.Get(0, cur).GetMember(unsafe.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tcb := func(cur realm) error {\n\t\tlaw = \u0026newLaw\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, ufmt.Sprintf(\"A new Law is proposed:\\n %v\", newLaw))\n\n\treturn dao.NewProposalRequest(\"Change Law Proposal\", \"This proposal is looking to change the actual govDAO Law\", e)\n}\n\nfunc NewUpgradeDaoImplRequest(cur realm, newDao dao.DAO, realmPkg, reason string) dao.ProposalRequest {\n\tmember, _ := memberstore.Get(0, cur).GetMember(unsafe.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tcb := func(cur realm) error {\n\t\t// dao.UpdateImpl() must be cross-called from v3/impl but\n\t\t// what calls this cb function is r/gov/dao.\n\t\t// therefore we must cross back into v3/impl and then\n\t\t// cross call dao.UpdateRequest().\n\t\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(newDao, []string{\"gno.land/r/gov/dao/v3/impl\", realmPkg}))\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\treturn dao.NewProposalRequest(\"Change DAO implementation\", \"This proposal is looking to change the actual govDAO implementation. Reason: \"+reason, e)\n}\n\nfunc NewAddMemberRequest(cur realm, addr address, tier string, portfolio string) dao.ProposalRequest {\n\t_, ok := memberstore.GetTier(tier)\n\tif !ok {\n\t\tpanic(\"provided tier does not exists\")\n\t}\n\n\tif tier != memberstore.T1 \u0026\u0026 tier != memberstore.T2 {\n\t\tpanic(\"Only T1 and T2 members can be added by proposal. To add a T3 member use AddMember function directly.\")\n\t}\n\n\tif portfolio == \"\" {\n\t\tpanic(\"A portfolio for the proposed member is required\")\n\t}\n\n\tmember, _ := memberstore.Get(0, cur).GetMember(unsafe.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tif member.InvitationPoints \u003c= 0 {\n\t\tpanic(\"proposer does not have enough invitation points for inviting new people to the board\")\n\t}\n\n\tcb := func(cur realm) error {\n\t\tmember.RemoveInvitationPoint()\n\t\terr := memberstore.Get(0, cur).SetMember(tier, addr, memberByTier(tier))\n\n\t\treturn err\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, ufmt.Sprintf(\"A new member with address %v is proposed to be on tier %v. Provided Portfolio information:\\n\\n%v\", addr, tier, portfolio))\n\n\tname := tryResolveAddr(addr)\n\treturn dao.NewProposalRequestWithFilter(\n\t\tufmt.Sprintf(\"New %s Member Proposal\", tier),\n\t\tufmt.Sprintf(\"This is a proposal to add `%s` to **%s**.\\n#### `%s`'s Portfolio:\\n\\n%s\\n\", name, tier, name, portfolio),\n\t\te,\n\t\tFilterByTier{Tier: tier},\n\t)\n}\n\nfunc NewWithdrawMemberRequest(cur realm, addr address, reason string) dao.ProposalRequest {\n\tmember, tier := memberstore.Get(0, cur).GetMember(addr)\n\tif member == nil {\n\t\tpanic(\"user we want to remove not found\")\n\t}\n\n\treason = strings.TrimSpace(reason)\n\tif tier == memberstore.T1 \u0026\u0026 reason == \"\" {\n\t\tpanic(\"T1 user removals must contains a reason.\")\n\t}\n\n\tcb := func(cur realm) error {\n\t\tmemberstore.Get(0, cur).RemoveMember(addr)\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, ufmt.Sprintf(\"Member with address %v will be withdrawn.\\n\\n REASON: %v.\", addr, reason))\n\n\treturn dao.NewProposalRequest(\n\t\t\"Member Withdrawal Proposal\",\n\t\tufmt.Sprintf(\"This is a proposal to remove %s from the GovDAO\", tryResolveAddr(addr)),\n\t\te,\n\t)\n}\n\nfunc NewPromoteMemberRequest(cur realm, addr address, fromTier string, toTier string) dao.ProposalRequest {\n\tcb := func(cur realm) error {\n\t\tprevTier := memberstore.Get(0, cur).RemoveMember(addr)\n\t\tif prevTier == \"\" {\n\t\t\tpanic(\"member not found, so cannot be promoted\")\n\t\t}\n\n\t\tif prevTier != fromTier {\n\t\t\tpanic(\"previous tier changed from the one indicated in the proposal\")\n\t\t}\n\n\t\terr := memberstore.Get(0, cur).SetMember(toTier, addr, memberByTier(toTier))\n\n\t\treturn err\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, ufmt.Sprintf(\"A new member with address %v will be promoted from tier %v to tier %v.\", addr, fromTier, toTier))\n\n\treturn dao.NewProposalRequestWithFilter(\n\t\t\"Member Promotion Proposal\",\n\t\tufmt.Sprintf(\"This is a proposal to promote %s from **%s** to **%s**.\", tryResolveAddr(addr), fromTier, toTier),\n\t\te,\n\t\tFilterByTier{Tier: toTier},\n\t)\n}\n\nfunc NewTreasuryPaymentRequest(cur realm, payment trs_pkg.Payment, reason string) dao.ProposalRequest {\n\tif !treasury.HasBanker(payment.BankerID()) {\n\t\tpanic(\"banker not registered in treasury with ID: \" + payment.BankerID())\n\t}\n\n\treason = strings.TrimSpace(reason)\n\tif reason == \"\" {\n\t\tpanic(\"treasury payment request requires a reason\")\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn panictoerr.PanicToError(func() {\n\t\t\ttreasury.Send(cross(cur), payment)\n\t\t})\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur,\n\t\tcb,\n\t\tufmt.Sprintf(\n\t\t\t\"A payment will be sent by the GovDAO treasury.\\n\\nReason: %s\\n\\nPayment: %s.\",\n\t\t\treason,\n\t\t\tpayment.String(),\n\t\t),\n\t)\n\n\treturn dao.NewProposalRequest(\n\t\t\"Treasury Payment\",\n\t\tufmt.Sprintf(\n\t\t\t\"This proposal is looking to send a payment using the treasury.\\n\\nReason: %s\\n\\nPayment: %s\",\n\t\t\treason,\n\t\t\tpayment.String(),\n\t\t),\n\t\te,\n\t)\n}\n\n// NewTreasuryGRC20TokensUpdate creates a proposal request to update the list of GRC20 tokens registry\n// keys used by the treasury. The new list, if voted and accepted, will overwrite the current one.\nfunc NewTreasuryGRC20TokensUpdate(cur realm, newTokenKeys []string) dao.ProposalRequest {\n\tif len(newTokenKeys) == 0 {\n\t\tpanic(\"the list of new tokens is empty\")\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn panictoerr.PanicToError(func() {\n\t\t\t// NOTE:: Consider checking if the newTokenKeys are already registered\n\t\t\t// in the grc20reg before updating the treasury tokens keys.\n\t\t\ttreasury.SetTokenKeys(cross(cur), newTokenKeys)\n\t\t})\n\t}\n\n\tbulletList := md.BulletList(newTokenKeys)\n\n\te := dao.NewSimpleExecutor(0, cur,\n\t\tcb,\n\t\tufmt.Sprintf(\n\t\t\t\"The list of GRC20 tokens used by the treasury will be updated.\\n\\nNew Token Keys:\\n%s.\\n\",\n\t\t\tbulletList,\n\t\t),\n\t)\n\n\treturn dao.NewProposalRequest(\n\t\t\"Treasury GRC20 Tokens Update\",\n\t\tufmt.Sprintf(\n\t\t\t\"This proposal is looking to update the list of GRC20 tokens used by the treasury.\\n\\nNew Token Keys:\\n%s\",\n\t\t\tbulletList,\n\t\t),\n\t\te,\n\t)\n}\n\nfunc memberByTier(tier string) *memberstore.Member {\n\tswitch tier {\n\tcase memberstore.T1:\n\t\tt, _ := memberstore.GetTier(memberstore.T1)\n\t\treturn memberstore.NewMember(t.InvitationPoints)\n\tcase memberstore.T2:\n\t\tt, _ := memberstore.GetTier(memberstore.T2)\n\t\treturn memberstore.NewMember(t.InvitationPoints)\n\tcase memberstore.T3:\n\t\tt, _ := memberstore.GetTier(memberstore.T3)\n\t\treturn memberstore.NewMember(t.InvitationPoints)\n\tdefault:\n\t\tpanic(\"member not found by the specified tier\")\n\t}\n}\n"},{"name":"render.gno","body":"package impl\n\nimport (\n\t\"chain/runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/helplink\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/bptree/v0/pager\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/sys/users\"\n)\n\ntype render struct {\n\trelativeRealmPath string\n\trouter            *mux.Router\n\tpssPager          *pager.Pager\n}\n\nfunc NewRender(d *GovDAO) *render {\n\tren := \u0026render{\n\t\tpssPager: pager.NewPager(d.pss.BPTree, 5, true),\n\t}\n\n\tr := mux.NewRouter()\n\n\t// Handlers use mux's rlm-aware shape: rlm is supplied at RenderRlm\n\t// dispatch time rather than captured at NewRender time. This lets\n\t// downstream crossing reads (dao.GetProposal etc.) use cross(rlm)\n\t// without relying on bare cross or restructuring the router.\n\tr.HandleFuncRlm(\"\", func(_ int, rlm realm, rw *mux.ResponseWriter, req *mux.Request) {\n\t\trw.Write(ren.renderActiveProposals(0, rlm, req.RawPath, d))\n\t})\n\n\tr.HandleFuncRlm(\"{pid}\", func(_ int, rlm realm, rw *mux.ResponseWriter, req *mux.Request) {\n\t\trw.Write(ren.renderProposalPage(0, rlm, req.GetVar(\"pid\"), d))\n\t})\n\n\tr.HandleFuncRlm(\"{pid}/votes\", func(_ int, rlm realm, rw *mux.ResponseWriter, req *mux.Request) {\n\t\trw.Write(ren.renderVotesForProposal(0, rlm, req.GetVar(\"pid\"), d))\n\t})\n\n\tren.router = r\n\n\treturn ren\n}\n\nfunc (ren *render) Render(_ int, rlm realm, pkgPath string, path string) string {\n\trelativePath, found := strings.CutPrefix(pkgPath, runtime.ChainDomain())\n\tif !found {\n\t\tpanic(ufmt.Sprintf(\n\t\t\t\"realm package with unexpected name found: %v in chain domain %v\",\n\t\t\tpkgPath, runtime.ChainDomain()))\n\t}\n\tren.relativeRealmPath = relativePath\n\treturn ren.router.RenderRlm(0, rlm, path)\n}\n\nfunc (ren *render) renderActiveProposals(_ int, rlm realm, url string, d *GovDAO) string {\n\tout := \"# GovDAO\\n\"\n\tout += \"## Members\\n\"\n\tout += \"[\u003e Go to Memberstore \u003c](/r/gov/dao/v3/memberstore)\\n\"\n\tout += \"## Proposals\\n\"\n\tpage := ren.pssPager.MustGetPageByPath(url)\n\tif len(page.Items) == 0 {\n\t\tout += \"\\nNo proposals yet.\\n\\n\"\n\t\treturn out\n\t}\n\n\tfor _, item := range page.Items {\n\t\tseqpid, err := seqid.FromString(item.Key)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tout += ren.renderProposalListItem(0, rlm, ufmt.Sprintf(\"%v\", int64(seqpid)), d)\n\t\tout += \"---\\n\\n\"\n\t}\n\n\tout += page.Picker(\"\")\n\n\treturn out\n}\n\nfunc (ren *render) renderProposalPage(_ int, rlm realm, sPid string, d *GovDAO) string {\n\tpid, err := strconv.ParseInt(sPid, 10, 64)\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tp, err := dao.GetProposal(dao.ProposalID(pid))\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Proposal not found\\n\\n%s\", err.Error())\n\t}\n\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tout := ufmt.Sprintf(\"## Prop #%v - %v\\n\", pid, md.EscapeText(p.Title()))\n\tout += \"Author: \" + tryResolveAddr(p.Author()) + \"\\n\\n\"\n\n\tout += p.Description()\n\tout += \"\\n\\n\"\n\n\t// Add executor metadata if available\n\tif p.ExecutorString() != \"\" {\n\t\tout += ufmt.Sprintf(`This proposal contains the following metadata:\n\n%s\n\nExecutor created in: %s\n`, p.ExecutorString(), p.ExecutorCreationRealm())\n\t\tout += \"\\n\\n\"\n\t}\n\n\tout += \"\\n\\n---\\n\\n\"\n\tout += ps.String(0, rlm)\n\tout += \"\\n\"\n\tout += ufmt.Sprintf(\"[Detailed voting list](%v:%v/votes)\", ren.relativeRealmPath, pid)\n\tout += \"\\n\\n---\\n\\n\"\n\n\tout += renderActionBar(ufmt.Sprintf(\"%v\", pid))\n\n\treturn out\n}\n\nfunc (ren *render) renderProposalListItem(_ int, rlm realm, sPid string, d *GovDAO) string {\n\tpid, err := strconv.ParseInt(sPid, 10, 64)\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tp, err := dao.GetProposal(dao.ProposalID(pid))\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Proposal not found\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tout := ufmt.Sprintf(\"### [Prop #%v - %v](%v:%v)\\n\", pid, md.EscapeText(p.Title()), ren.relativeRealmPath, pid)\n\tout += ufmt.Sprintf(\"Author: %s\\n\\n\", tryResolveAddr(p.Author()))\n\n\tout += \"Status: \" + getPropStatus(ps)\n\tout += \"\\n\\n\"\n\n\tout += \"Tiers eligible to vote: \"\n\tout += strings.Join(ps.TiersAllowedToVote, \", \")\n\n\tout += \"\\n\\n\"\n\treturn out\n}\n\nfunc (ren *render) renderVotesForProposal(_ int, rlm realm, sPid string, d *GovDAO) string {\n\tpid, err := strconv.ParseInt(sPid, 10, 64)\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tif ps == nil {\n\t\treturn ufmt.Sprintf(\"# Proposal not found\\n\\nProposal %v does not exist.\", pid)\n\t}\n\n\tout := \"\"\n\tout += ufmt.Sprintf(\"# Proposal #%v - Vote List\\n\\n\", pid)\n\tout += StringifyVotes(0, rlm, ps)\n\n\treturn out\n}\n\nfunc isPropActive(ps *proposalStatus) bool {\n\treturn !ps.Accepted \u0026\u0026 !ps.Denied\n}\n\nfunc getPropStatus(ps *proposalStatus) string {\n\tif ps == nil {\n\t\treturn \"UNKNOWN\"\n\t}\n\tif ps.Accepted {\n\t\treturn \"ACCEPTED\"\n\t} else if ps.Denied {\n\t\treturn \"REJECTED\"\n\t}\n\treturn \"ACTIVE\"\n}\n\nfunc renderActionBar(sPid string) string {\n\tout := \"### Actions\\n\"\n\n\tproxy := helplink.Realm(\"gno.land/r/gov/dao\")\n\tout += proxy.Func(\"Vote YES\", \"MustVoteOnProposalSimple\", \"pid\", sPid, \"option\", \"YES\") + \" | \"\n\tout += proxy.Func(\"Vote NO\", \"MustVoteOnProposalSimple\", \"pid\", sPid, \"option\", \"NO\") + \" | \"\n\tout += proxy.Func(\"Vote ABSTAIN\", \"MustVoteOnProposalSimple\", \"pid\", sPid, \"option\", \"ABSTAIN\")\n\n\tout += \"\\n\\n\"\n\tout += \"WARNING: Please double check transaction data before voting.\"\n\treturn out\n}\n\nfunc tryResolveAddr(addr address) string {\n\tuserData := users.ResolveAddress(addr)\n\tif userData == nil {\n\t\treturn addr.String()\n\t}\n\treturn userData.RenderLink(\"\")\n}\n"},{"name":"types.gno","body":"package impl\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\ntype Law struct {\n\tSupermajority float64\n}\n\nfunc NewLaw(supermajority float64) Law {\n\treturn Law{Supermajority: supermajority}\n}\n\nfunc (l *Law) String() string {\n\treturn ufmt.Sprintf(\"This law contains the following data:\\n\\n- Supermajority: %v%%\", l.Supermajority)\n}\n\n// ProposalsStatuses contains the status of all the proposals indexed by the proposal ID.\ntype ProposalsStatuses struct {\n\t*bptree.BPTree // map[int]*proposalStatus\n}\n\nfunc NewProposalsStatuses() ProposalsStatuses {\n\treturn ProposalsStatuses{bptree.NewBPTree32()}\n}\n\nfunc (pss ProposalsStatuses) GetStatus(id dao.ProposalID) *proposalStatus {\n\tif pss.BPTree == nil {\n\t\treturn nil\n\t}\n\n\tpids := id.String()\n\tpsv := pss.Get(pids)\n\tif psv == nil {\n\t\treturn nil\n\t}\n\n\tps, ok := psv.(*proposalStatus)\n\tif !ok {\n\t\tpanic(\"ProposalsStatuses must contains only proposalStatus types\")\n\t}\n\n\treturn ps\n}\n\ntype proposalStatus struct {\n\tYesVotes     memberstore.MembersByTier\n\tNoVotes      memberstore.MembersByTier\n\tAbstainVotes memberstore.MembersByTier\n\tAllVotes     memberstore.MembersByTier\n\n\tAccepted bool\n\tDenied   bool\n\n\tDeniedReason string\n\n\tTiersAllowedToVote []string\n}\n\nfunc getMembers(cur realm) memberstore.MembersByTier {\n\treturn memberstore.Get(0, cur)\n}\n\nfunc newEmptyVoteStore() memberstore.MembersByTier {\n\tmbt := memberstore.NewMembersByTier()\n\tmbt.SetTier(memberstore.T1)\n\tmbt.SetTier(memberstore.T2)\n\tmbt.SetTier(memberstore.T3)\n\treturn mbt\n}\n\nfunc newProposalStatus(allowedToVote []string) *proposalStatus {\n\treturn \u0026proposalStatus{\n\t\tYesVotes:           newEmptyVoteStore(),\n\t\tNoVotes:            newEmptyVoteStore(),\n\t\tAbstainVotes:       newEmptyVoteStore(),\n\t\tAllVotes:           newEmptyVoteStore(),\n\t\tTiersAllowedToVote: allowedToVote,\n\t}\n}\n\n// totalPower computes the total voting power dynamically from current members\n// rather than using a snapshot. See https://github.com/gnolang/gno/pull/5271#discussion_r2952523023\n//\n// Non-crossing: rlm is threaded into the crossing getMembers(cross(rlm))\n// call. The proposalStatus methods that compose this (votePowerPercent,\n// YesPercent etc., String) all take `_ int, rlm realm` for the same reason.\nfunc (ps *proposalStatus) totalPower(_ int, rlm realm) float64 {\n\tmembers := getMembers(cross(rlm))\n\tvar tp float64\n\tfor _, tn := range ps.TiersAllowedToVote {\n\t\tpower := memberstore.GetTierPower(tn, members)\n\t\ttp += power * float64(members.GetTierSize(tn))\n\t}\n\treturn tp\n}\n\nfunc (ps *proposalStatus) votePowerPercent(_ int, rlm realm, votes memberstore.MembersByTier) float64 {\n\tmembers := getMembers(cross(rlm))\n\tvar vp float64\n\tmemberstore.IterateTiers(func(tn string, tier memberstore.Tier) bool {\n\t\tpower := memberstore.GetTierPower(tn, members)\n\t\tts := votes.GetTierSize(tn)\n\t\tvp = vp + (power * float64(ts))\n\t\treturn false\n\t})\n\ttp := ps.totalPower(0, rlm)\n\tif tp == 0 {\n\t\treturn 0\n\t}\n\treturn (vp / tp) * 100\n}\n\nfunc (ps *proposalStatus) YesPercent(_ int, rlm realm) float64 {\n\treturn ps.votePowerPercent(0, rlm, ps.YesVotes)\n}\n\nfunc (ps *proposalStatus) NoPercent(_ int, rlm realm) float64 {\n\treturn ps.votePowerPercent(0, rlm, ps.NoVotes)\n}\n\nfunc (ps *proposalStatus) AbstainPercent(_ int, rlm realm) float64 {\n\treturn ps.votePowerPercent(0, rlm, ps.AbstainVotes)\n}\n\nfunc (ps *proposalStatus) IsAllowed(tier string) bool {\n\tfor _, ta := range ps.TiersAllowedToVote {\n\t\tif ta == tier {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// XXX: can be optimized by passing down total power to Yes/No/Abstain percent fn to avoid re-computing\nfunc (ps *proposalStatus) String(_ int, rlm realm) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"### Stats\\n\")\n\n\tif ps.Accepted {\n\t\tsb.WriteString(\"- **PROPOSAL HAS BEEN ACCEPTED**\\n\")\n\t} else if ps.Denied {\n\t\tsb.WriteString(\"- **PROPOSAL HAS BEEN DENIED**\\n\")\n\t\tif ps.DeniedReason != \"\" {\n\t\t\tsb.WriteString(\"REASON: \")\n\t\t\tsb.WriteString(ps.DeniedReason)\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t} else {\n\t\tsb.WriteString(\"- **Proposal is open for votes**\\n\")\n\t}\n\n\tsb.WriteString(\"- Tiers eligible to vote: \")\n\tsb.WriteString(strings.Join(ps.TiersAllowedToVote, \", \"))\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(ufmt.Sprintf(\"- YES PERCENT: %v%%\\n\", ps.YesPercent(0, rlm)))\n\tsb.WriteString(ufmt.Sprintf(\"- NO PERCENT: %v%%\\n\", ps.NoPercent(0, rlm)))\n\tsb.WriteString(ufmt.Sprintf(\"- ABSTAIN PERCENT: %v%%\\n\", ps.AbstainPercent(0, rlm)))\n\n\treturn sb.String()\n}\n\nfunc StringifyVotes(_ int, rlm realm, ps *proposalStatus) string {\n\tvar sb strings.Builder\n\n\twriteVotes(0, rlm, \u0026sb, ps.YesVotes, \"YES\")\n\twriteVotes(0, rlm, \u0026sb, ps.NoVotes, \"NO\")\n\twriteVotes(0, rlm, \u0026sb, ps.AbstainVotes, \"ABSTAIN\")\n\n\tif sb.String() == \"\" {\n\t\treturn \"No one voted yet.\"\n\t}\n\n\treturn sb.String()\n}\n\nfunc writeVotes(_ int, rlm realm, sb *strings.Builder, t memberstore.MembersByTier, title string) {\n\tif t.Size() == 0 {\n\t\treturn\n\t}\n\tmembers := getMembers(cross(rlm))\n\tt.Iterate(\"\", \"\", func(tn string, value interface{}) bool {\n\t\t_, ok := memberstore.GetTier(tn)\n\t\tif !ok {\n\t\t\tpanic(\"tier not found\")\n\t\t}\n\n\t\tpower := memberstore.GetTierPower(tn, members)\n\n\t\tsb.WriteString(ufmt.Sprintf(\"%v from %v (VPPM %v):\\n\\n\", title, tn, power))\n\t\tms, _ := value.(*bptree.BPTree)\n\t\tms.Iterate(\"\", \"\", func(addr string, _ interface{}) bool {\n\t\t\tsb.WriteString(\"- \" + tryResolveAddr(address(addr)) + \"\\n\")\n\t\t\treturn false\n\t\t})\n\n\t\tsb.WriteString(\"\\n\")\n\n\t\treturn false\n\t})\n}\n\nfunc StringifyProposal(p *dao.Proposal) string {\n\tout := ufmt.Sprintf(`\n### Title: %s\n\n### Proposed by: %s\n\n%s\n`, p.Title(), p.Author(), p.Description())\n\n\tif p.ExecutorString() != \"\" {\n\t\tout += ufmt.Sprintf(`\nThis proposal contains the following metadata:\n\n%s\n\nExecutor created in: %s\n`, p.ExecutorString(), p.ExecutorCreationRealm())\n\t}\n\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"init","path":"gno.land/r/gov/dao/v3/init","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/v3/init\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"},{"name":"init.gno","body":"package init\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/impl\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nfunc Init(cur realm) {\n\tassertIsDevChain()\n\n\t// This is needed because state is saved between unit tests,\n\t// and we want to avoid having real members used on tests\n\tmemberstore.Get(0, cur).DeleteAll()\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{\"gno.land/r/gov/dao/v3/impl\"}))\n}\n\nfunc InitWithUsers(cur realm, addrs ...address) {\n\tassertIsDevChain()\n\n\t// This is needed because state is saved between unit tests,\n\t// and we want to avoid having real members used on tests\n\tmemberstore.Get(0, cur).DeleteAll()\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tfor _, a := range addrs {\n\t\tif !a.IsValid() {\n\t\t\tpanic(\"invalid address: \" + a.String())\n\t\t}\n\t\tmemberstore.Get(0, cur).SetMember(memberstore.T1, a, memberstore.NewMember(3))\n\t}\n\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.NewGovDAO(), []string{\"gno.land/r/gov/dao/v3/impl\"}))\n}\n\nfunc assertIsDevChain() {\n\tchainID := runtime.ChainID()\n\tif chainID != \"dev\" \u0026\u0026 chainID != \"tendermint_test\" {\n\t\tpanic(\"unauthorized\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"p_closurecap","path":"gno.land/p/demo/tests/p_closurecap","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/p_closurecap\"\ngno = \"0.9\"\n"},{"name":"p_closurecap.gno","body":"// Package p_closurecap exercises the \"var stopped bool in\n// memberStorage.IterateByOffset\" pattern from boards2/commondao:\n// a method declares a local var that an inline closure captures and\n// writes to. Without the unreal-HIV exception in the readonly check,\n// this failed across borrow-realm transitions because the HIV's PkgID\n// stamp records the alloc-site realm, but the closure body may run\n// under a different borrowed realm — the check would fire on a write\n// the closure itself made to its own captured slot.\npackage p_closurecap\n\n// Inner is the receiver of loop(). When Inner lives in a different\n// realm than the Outer-method's caller realm, PushFrameCall's borrow\n// rule 2 shifts m.Realm to Inner's realm for the entire body of loop()\n// (and the synchronously-invoked closure). That shift is what makes\n// HIV.PkgID (stamped at var stopped's alloc) differ from m.Realm at\n// the closure's write site.\ntype Inner struct {\n\tN int\n}\n\n// Outer is the var-stopped pattern. The inline closure captures\n// `stopped` and writes to it. Returns true if `fn` ever returned true.\n//\n// The key shape: Outer is a top-level /p/ func (no receiver, so no\n// borrow on entry — HIV stamps with caller's realm), but it delegates\n// to `inn.loop(...)`, where `inn` is supplied by the caller and may\n// live in a different realm. inn.loop's borrow flips m.Realm to inn's\n// realm; the inline closure then writes to `stopped` under that\n// borrowed realm.\nfunc Outer(inn *Inner, count int, fn func(i int) bool) bool {\n\tvar stopped bool\n\tinn.loop(count, func(i int) bool {\n\t\tstopped = fn(i)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// loop is a /p/ method on *Inner. As a receiver-method on a /p/ type,\n// PushFrameCall's borrow rule 2 fires here: m.Realm becomes\n// Inner.PkgID's realm for the duration of loop().\nfunc (in *Inner) loop(count int, cb func(i int) bool) {\n\tfor i := 0; i \u003c count; i++ {\n\t\tif cb(i) {\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// MakeCounter returns a closure that captures a local int. The\n// returned closure can be stored in /r/ state — verifying that a\n// persisted closure-capture HIV (whose FuncLit lives in /p/) is still\n// writable when invoked from a foreign realm context.\nfunc MakeCounter(start int) func() int {\n\tc := start\n\treturn func() int {\n\t\tc++\n\t\treturn c\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"persistedcap","path":"gno.land/r/tests/vm/persistedcap","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/persistedcap\"\ngno = \"0.9\"\n"},{"name":"persistedcap.gno","body":"// Package persistedcap stores a closure (constructed by a /p/ factory)\n// at init, persisting the closure's captured HIV as part of this\n// realm's state. Foreign-realm callers can grab the closure via\n// GetCounter() and invoke it; the closure body's write to its\n// captured HIV is REJECTED by the readonly check because the HIV is\n// real (NewTime\u003e0) and belongs to a different realm than the caller's\n// m.Realm. The unreal-HIV exception only applies while the HIV is\n// transient — persistence elevates it to a normal realm-owned slot.\npackage persistedcap\n\nimport \"gno.land/p/demo/tests/p_closurecap\"\n\nvar counter func() int\n\nfunc init() {\n\tcounter = p_closurecap.MakeCounter(0)\n}\n\n// GetCounter returns the persisted closure. The caller invokes it\n// directly — because the closure's FuncLit lives in /p/ p_closurecap,\n// PushFrameCall's borrow rule does NOT switch m.Realm to persistedcap\n// at invocation. The closure body's write to the captured HIV (PkgID\n// = persistedcap, persisted) therefore runs under the caller's\n// m.Realm, which is the persisted-closure-capture cross-realm case.\n//\n// SECURITY: deliberate VM-parity-test infrastructure — do NOT copy this\n// pattern in production code. See gno.land/r/tests/vm/crossrealm\n// Closure for the broader rationale.\nfunc GetCounter() func() int {\n\treturn counter\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"subtests","path":"gno.land/r/tests/vm/subtests","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/subtests\"\ngno = \"0.9\"\n"},{"name":"subtests.gno","body":"package subtests\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n)\n\nfunc GetCurrentRealm(cur realm) runtime.Realm {\n\treturn unsafe.CurrentRealm()\n}\n\nfunc GetPreviousRealm(cur realm) runtime.Realm {\n\treturn unsafe.PreviousRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc CallAssertOriginCall(cur realm) {\n\truntime.AssertOriginCall()\n}\n\nfunc CallIsOriginCall(cur realm) bool {\n\treturn unsafe.PreviousRealm().IsUser()\n}\n\nfunc BankerOriginSend(cur realm) string {\n\treturn unsafe.OriginSend().String()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"cla","path":"gno.land/r/sys/cla","files":[{"name":"admin.gno","body":"package cla\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/helplink\"\n\t\"gno.land/r/gov/dao\"\n)\n\nconst RequiredHashChangedEvent = \"CLARequiredHashChanged\"\n\n// ProposeNewCLA creates a govdao proposal to update the CLA document hash and URL.\n// When executed, it resets all existing signatures.\n// Propose an empty hash to disable CLA enforcement.\nfunc ProposeNewCLA(cur realm, newHash, newURL string) dao.ProposalRequest {\n\tcb := func(cur realm) error {\n\t\tsetRequiredHash(newHash)\n\t\tclaURL = newURL\n\t\treturn nil\n\t}\n\n\tdesc := \"Propose updating the CLA requirement.\\n\\n\"\n\tif requiredHash != \"\" {\n\t\tdesc += \"Current hash: \" + requiredHash + \"\\n\"\n\t}\n\tif claURL != \"\" {\n\t\tdesc += \"Current URL: \" + claURL + \"\\n\"\n\t}\n\tif newHash != \"\" {\n\t\tdesc += \"New hash: \" + newHash + \"\\n\"\n\t}\n\tif newURL != \"\" {\n\t\tdesc += \"New URL: \" + newURL + \"\\n\"\n\t}\n\tif newHash == \"\" {\n\t\tdesc += \"This proposal disables CLA enforcement.\\n\"\n\t}\n\n\treturn dao.NewProposalRequest(\n\t\t\"Update CLA requirement\",\n\t\tdesc,\n\t\tdao.NewSimpleExecutor(0, cur, cb, helplink.Realm(\"gno.land/r/sys/cla\").Home()),\n\t)\n}\n\nfunc setRequiredHash(newHash string) {\n\tprevHash := requiredHash\n\trequiredHash = newHash\n\tsignatures = addrset.Set{} // reset all signatures\n\n\tchain.Emit(\n\t\tRequiredHashChangedEvent,\n\t\t\"from\", prevHash,\n\t\t\"to\", newHash,\n\t)\n}\n"},{"name":"cla.gno","body":"package cla\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/moul/addrset\"\n)\n\nconst SignedEvent = \"CLASigned\"\n\nvar (\n\trequiredHash string // SHA256 hash of the CLA document; empty = enforcement disabled\n\tclaURL       string // URL where the CLA document can be found\n\tsignatures   addrset.Set\n)\n\n// Sign records a CLA signature for the caller.\n// The hash must match the current required hash.\nfunc Sign(cur realm, hash string) {\n\tif hash != requiredHash {\n\t\tpanic(\"hash does not match required CLA hash\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tsignatures.Add(caller)\n\n\tchain.Emit(\n\t\tSignedEvent,\n\t\t\"signer\", caller.String(),\n\t\t\"hash\", hash,\n\t)\n}\n\n// HasValidSignature checks if an address has signed the current required CLA.\n// Returns true if CLA enforcement is disabled (requiredHash == \"\"),\n// or if the address has signed.\nfunc HasValidSignature(addr address) bool {\n\tif requiredHash == \"\" {\n\t\treturn true\n\t}\n\treturn signatures.Has(addr)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/cla\"\ngno = \"0.9\"\n"},{"name":"render.gno","body":"package cla\n\nimport (\n\t\"gno.land/p/moul/helplink\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc Render(path string) string {\n\tout := md.H1(\"Contributor License Agreement (CLA)\")\n\n\tout += md.Paragraph(\"A Contributor License Agreement (CLA) must be signed before deploying packages.\")\n\tout += md.Paragraph(\n\t\t\"The Agreement governs Contributions uploaded, published, or made available \" +\n\t\t\t\"for execution on the Gno.land blockchain network, and the \" +\n\t\t\t\"related software and repositories used to publish such Contributions.\",\n\t)\n\n\tif requiredHash == \"\" {\n\t\tout += md.HorizontalRule()\n\t\tout += md.H2(\"Status\")\n\t\tout += md.Paragraph(md.Bold(\"CLA enforcement is currently DISABLED.\"))\n\t\tout += md.Paragraph(\"All package deployments are allowed.\")\n\t\treturn out\n\t}\n\n\tout += md.HorizontalRule()\n\tout += md.H2(\"Status\")\n\tout += md.Paragraph(md.Bold(\"CLA enforcement is ENABLED\"))\n\n\tif claURL != \"\" {\n\t\tout += md.Paragraph(\"You can read the full agreement here: \" + md.Link(claURL, claURL))\n\t}\n\n\ttable := mdtable.Table{Headers: []string{\"\", \"\"}}\n\ttable.Append([]string{md.Bold(\"Required Hash\"), md.InlineCode(requiredHash)})\n\ttable.Append([]string{md.Bold(\"Signers\"), ufmt.Sprintf(\"%d contributor(s)\", signatures.Size())})\n\tout += table.String()\n\n\tout += md.H3(\"Actions\")\n\tout += md.Paragraph(helplink.Func(\"Sign CLA\", \"Sign\", \"hash\", requiredHash))\n\treturn out\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"once","path":"gno.land/p/moul/once","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/once\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"once.gno","body":"// Package once provides utilities for one-time execution patterns.\n// It extends the concept of sync.Once with error handling and panic options.\npackage once\n\nimport (\n\t\"errors\"\n)\n\n// Once represents a one-time execution guard\ntype Once struct {\n\tdone    bool\n\terr     error\n\tpaniced bool\n\tvalue   any // stores the result of the execution\n}\n\n// New creates a new Once instance\nfunc New() *Once {\n\treturn \u0026Once{}\n}\n\n// Do executes fn only once and returns nil on subsequent calls\nfunc (o *Once) Do(fn func()) {\n\tif o.done {\n\t\treturn\n\t}\n\tdefer func() { o.done = true }()\n\tfn()\n}\n\n// DoErr executes fn only once and returns the same error on subsequent calls\nfunc (o *Once) DoErr(fn func() error) error {\n\tif o.done {\n\t\treturn o.err\n\t}\n\tdefer func() { o.done = true }()\n\to.err = fn()\n\treturn o.err\n}\n\n// DoOrPanic executes fn only once and panics on subsequent calls\nfunc (o *Once) DoOrPanic(fn func()) {\n\tif o.done {\n\t\tpanic(\"once: multiple execution attempted\")\n\t}\n\tdefer func() { o.done = true }()\n\tfn()\n}\n\n// DoValue executes fn only once and returns its value, subsequent calls return the cached value\nfunc (o *Once) DoValue(fn func() any) any {\n\tif o.done {\n\t\treturn o.value\n\t}\n\tdefer func() { o.done = true }()\n\to.value = fn()\n\treturn o.value\n}\n\n// DoValueErr executes fn only once and returns its value and error\n// Subsequent calls return the cached value and error\nfunc (o *Once) DoValueErr(fn func() (any, error)) (any, error) {\n\tif o.done {\n\t\treturn o.value, o.err\n\t}\n\tdefer func() { o.done = true }()\n\to.value, o.err = fn()\n\treturn o.value, o.err\n}\n\n// Reset resets the Once instance to its initial state\n// This is mainly useful for testing purposes\nfunc (o *Once) Reset() {\n\to.done = false\n\to.err = nil\n\to.paniced = false\n\to.value = nil\n}\n\n// IsDone returns whether the Once has been executed\nfunc (o *Once) IsDone() bool {\n\treturn o.done\n}\n\n// Error returns the error from the last execution if any\nfunc (o *Once) Error() error {\n\treturn o.err\n}\n\nvar (\n\tErrNotExecuted = errors.New(\"once: not executed yet\")\n)\n\n// Value returns the stored value and an error if not executed yet\nfunc (o *Once) Value() (any, error) {\n\tif !o.done {\n\t\treturn nil, ErrNotExecuted\n\t}\n\treturn o.value, nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"authz","path":"gno.land/p/moul/authz","files":[{"name":"authz.gno","body":"// Package authz provides flexible authorization control for privileged actions.\n//\n// # Authorization Strategies\n//\n// The package supports multiple authorization strategies:\n//   - Member-based: Single user or team of users\n//   - Contract-based: Async authorization (e.g., via DAO)\n//   - Auto-accept: Allow all actions\n//   - Drop: Deny all actions\n//\n// Core Components\n//\n//   - Authority interface: Base interface implemented by all authorities\n//   - Authorizer: Main wrapper object for authority management\n//   - MemberAuthority: Manages authorized addresses\n//   - ContractAuthority: Delegates to another contract\n//   - AutoAcceptAuthority: Accepts all actions\n//   - DroppedAuthority: Denies all actions\n//\n// Quick Start\n//\n//\t// Initialize with contract deployer as authority\n//\tvar member address(...)\n//\tvar auth = authz.NewWithMembers(member)\n//\n//\t// Create functions that require authorization\n//\tfunc UpdateConfig(cur realm, newValue string) error {\n//\t\treturn auth.DoByPrevious(0, cur, \"update_config\", func() error {\n//\t\t\tconfig = newValue\n//\t\t\treturn nil\n//\t\t})\n//\t}\n//\n// See example_test.gno for more usage examples.\npackage authz\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/once\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Authorizer is the main wrapper object that handles authority management.\n// It is configured with a replaceable Authority implementation.\ntype Authorizer struct {\n\tauth Authority\n}\n\n// Authority represents an entity that can authorize privileged actions.\n// It is implemented by MemberAuthority, ContractAuthority, AutoAcceptAuthority,\n// and DroppedAuthority.\n//\n// Authority is the canonical safe shape for cross-package authority\n// interfaces: methods are address-typed (no realm/cur crosses the interface\n// boundary), and consumers correctly derive `caller` from\n// `cur.Previous().Address()` under `rlm.IsCurrent()` before invoking\n// Authorize. No cur-leak (class 1) is possible through this interface.\n//\n// However, two RESIDUAL RISKS apply:\n//\n//   - Class-3 impl-substitution: NewWithAuthority and Authorizer.Transfer\n//     accept any Authority impl. A malicious Authority can always-approve\n//     (silent privilege escalation) or always-deny (denial-of-service).\n//     Consumers should pass canonical impls from this package\n//     (MemberAuthority, ContractAuthority, AutoAcceptAuthority,\n//     DroppedAuthority) unless they have explicit reason to register a\n//     foreign impl. We do not expose an IsCanonicalAuthority allowlist\n//     because the package is intentionally extensible — third-party impls\n//     are the design intent.\n//\n//   - Class-4 closed-over-authority: NewContractAuthority and\n//     NewRestrictedContractAuthority capture a caller-supplied\n//     PrivilegedActionHandler closure. The handler runs synchronously\n//     inside Authorize with the consumer's authority. A hostile handler\n//     can swallow actions, log the caller, or execute arbitrary code\n//     under the consumer's frame. Register only trusted handler functions.\n//     See r/gnops/valopers/init.gno for the realistic registration shape.\n//\n// We do NOT seal Authority via an unexported marker method — that pattern\n// is bypassable via embedding in Gno; see\n// p/test/seal/filetests/z_seal_*_filetest.gno for the four bypass tests.\ntype Authority interface {\n\t// Authorize executes a privileged action if the caller is authorized\n\t// Additional args can be provided for context (e.g., for proposal creation)\n\tAuthorize(caller address, title string, action PrivilegedAction, args ...any) error\n\n\t// String returns a human-readable description of the authority\n\tString() string\n}\n\n// PrivilegedAction defines a function that performs a privileged action.\ntype PrivilegedAction func() error\n\n// PrivilegedActionHandler is called by contract-based authorities to handle\n// privileged actions.\ntype PrivilegedActionHandler func(title string, action PrivilegedAction) error\n\n// NewWithMembers creates a new Authorizer whose authority is a\n// MemberAuthority containing the given addresses. Callers express\n// authority intent at the call site:\n//\n//\t// \"auth realm is the authority\"\n//\ta := authz.NewWithMembers(cur.Address())\n//\n//\t// \"previous realm is the authority\" (from a crossing function)\n//\ta := authz.NewWithMembers(cur.Previous().Address())\n//\n//\t// \"EOA caller is the authority\" (from init(cur realm))\n//\tif !cur.Previous().IsUserCall() {\n//\t    panic(\"realm must be initialized by EOA\")\n//\t}\n//\ta := authz.NewWithMembers(cur.Previous().Address())\n//\n// This replaces the previous NewWithCurrent / NewWithPrevious /\n// NewWithOrigin sugar — those baked runtime.{Current,Previous,Origin}\n// reads into the constructor, which (a) prevented use from package-\n// level var initializers, (b) made the EOA-origin check inside\n// NewWithOrigin an indirect address comparison rather than the\n// straightforward IsUserCall predicate, and (c) coupled the\n// constructor to the runtime walks the rest of the migration is\n// moving away from.\nfunc NewWithMembers(addrs ...address) *Authorizer {\n\treturn \u0026Authorizer{\n\t\tauth: NewMemberAuthority(addrs...),\n\t}\n}\n\n// NewWithAuthority creates a new Authorizer with a specific authority.\n//\n// SECURITY: `authority` is an open-interface input — any value satisfying\n// Authority is accepted. A malicious impl can always-approve (privilege\n// escalation) or always-deny (DoS). Prefer canonical impls from this\n// package (NewMemberAuthority, NewContractAuthority, NewAutoAcceptAuthority,\n// NewDroppedAuthority) unless you specifically need a foreign impl.\nfunc NewWithAuthority(authority Authority) *Authorizer {\n\treturn \u0026Authorizer{\n\t\tauth: authority,\n\t}\n}\n\n// Authority returns the auth authority implementation\nfunc (a *Authorizer) Authority() Authority {\n\treturn a.auth\n}\n\n// Transfer changes the auth authority after validation. rlm must be the\n// caller's own captured cur (asserted via rlm.IsCurrent()); the\n// principal is rlm.Previous().Address(). Closes the address-parameter\n// forgery: an external realm cannot supply Owner() as `caller` to\n// bypass the underlying Authority's check.\n//\n// SECURITY (runtime substitution): once the current authority approves a\n// Transfer, the new authority is installed and effective on the next call.\n// If an attacker ever becomes the authority — even briefly — they can\n// install a permanent DroppedAuthority (DoS) or an AutoAcceptAuthority\n// (privilege escalation). Consumers concerned about this should wrap\n// Transfer with a one-shot guard or a quorum/cooldown check.\n//\n// `newAuthority` is also an open-interface input — see NewWithAuthority's\n// Class-3 caveat. Pass canonical impls.\nfunc (a *Authorizer) Transfer(_ int, rlm realm, newAuthority Authority) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\tcaller := rlm.Previous().Address()\n\treturn a.auth.Authorize(caller, \"transfer_authority\", func() error {\n\t\ta.auth = newAuthority\n\t\treturn nil\n\t})\n}\n\n// DoByCurrent executes a privileged action authorized as `rlm`. `rlm`\n// must be the caller's own live cur (asserted via rlm.IsCurrent());\n// the authorized principal is `rlm.Address()`. To authorize as the\n// realm that called your function, use `DoByPrevious`.\n//\n//\tauth.DoByCurrent(0, cur, \"update_config\", func() error { ... })    // current realm authorizes\n//\tauth.DoByPrevious(0, cur, \"update_config\", func() error { ... })   // calling realm authorizes\n//\n// The `_ int` first parameter is a deliberate sentinel that pushes\n// `rlm realm` past the first-arg position so DoByCurrent stays a\n// non-crossing method — otherwise it would be a crossing method and\n// rlm.Previous() inside would resolve one realm deeper than the caller\n// intended.\n//\n// SECURITY: the IsCurrent guard closes Class-2 designation forgery (see\n// docs/resources/gno-security.md). A realm value's .Address() is set\n// when the value is minted at a crossing frame; the value can in\n// principle be stored and replayed. Without IsCurrent, a hostile realm\n// could capture a high-privilege realm's cur.Previous() (e.g., when\n// that realm called into it) and later pass the stored value here to\n// authorize actions as that realm. IsCurrent rejects stale captures by\n// requiring the value to match the topmost live crossing frame's cur.\nfunc (a *Authorizer) DoByCurrent(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\treturn a.auth.Authorize(rlm.Address(), title, action, args...)\n}\n\n// DoByPrevious executes a privileged action authorized as the realm\n// that called the function invoking DoByPrevious. `rlm` must be the\n// caller's own live cur; the principal is derived as\n// `rlm.Previous().Address()`. Mirrors the Transfer/AddMember pattern:\n// always take live cur, derive the caller-of-caller internally rather\n// than accepting a stored/forwarded realm value.\nfunc (a *Authorizer) DoByPrevious(_ int, rlm realm, title string, action PrivilegedAction, args ...any) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\treturn a.auth.Authorize(rlm.Previous().Address(), title, action, args...)\n}\n\n// String returns a string representation of the auth authority\nfunc (a *Authorizer) String() string {\n\tauthStr := a.auth.String()\n\n\tswitch a.auth.(type) {\n\tcase *MemberAuthority:\n\tcase *ContractAuthority:\n\tcase *AutoAcceptAuthority:\n\tcase *droppedAuthority:\n\tdefault:\n\t\t// this way official \"dropped\" is different from \"*custom*: dropped\" (autoclaimed).\n\t\treturn ufmt.Sprintf(\"custom_authority[%s]\", authStr)\n\t}\n\treturn authStr\n}\n\n// MemberAuthority is the default implementation using addrset for member\n// management.\ntype MemberAuthority struct {\n\tmembers addrset.Set\n}\n\nfunc NewMemberAuthority(members ...address) *MemberAuthority {\n\tauth := \u0026MemberAuthority{}\n\tfor _, addr := range members {\n\t\tauth.members.Add(addr)\n\t}\n\treturn auth\n}\n\nfunc (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\tif !a.members.Has(caller) {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\n\tif err := action(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (a *MemberAuthority) String() string {\n\taddrs := []string{}\n\ta.members.Tree().Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\taddrs = append(addrs, key)\n\t\treturn false\n\t})\n\taddrsStr := strings.Join(addrs, \",\")\n\treturn ufmt.Sprintf(\"member_authority[%s]\", addrsStr)\n}\n\n// AddMember adds a new member to the authority. rlm must be the caller's\n// own captured cur; the principal is rlm.Previous().Address() and must\n// already be a member. The IsCurrent guard closes the forgery where an\n// external realm passes Owner() as caller to bypass members.Has(caller).\nfunc (a *MemberAuthority) AddMember(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\tcaller := rlm.Previous().Address()\n\treturn a.Authorize(caller, \"add_member\", func() error {\n\t\ta.members.Add(addr)\n\t\treturn nil\n\t})\n}\n\n// AddMembers adds a list of members to the authority. Same rlm contract\n// as AddMember.\nfunc (a *MemberAuthority) AddMembers(_ int, rlm realm, addrs ...address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\tcaller := rlm.Previous().Address()\n\treturn a.Authorize(caller, \"add_members\", func() error {\n\t\tfor _, addr := range addrs {\n\t\t\ta.members.Add(addr)\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// RemoveMember removes a member from the authority. Same rlm contract\n// as AddMember.\nfunc (a *MemberAuthority) RemoveMember(_ int, rlm realm, addr address) error {\n\tif !rlm.IsCurrent() {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\tcaller := rlm.Previous().Address()\n\treturn a.Authorize(caller, \"remove_member\", func() error {\n\t\ta.members.Remove(addr)\n\t\treturn nil\n\t})\n}\n\n// Tree returns a read-only view of the members tree\nfunc (a *MemberAuthority) Tree() *rotree.ReadOnlyTree {\n\ttree := a.members.Tree().(*avl.Tree)\n\treturn rotree.Wrap(tree, nil)\n}\n\n// Has checks if the given address is a member of the authority\nfunc (a *MemberAuthority) Has(addr address) bool {\n\treturn a.members.Has(addr)\n}\n\n// ContractAuthority implements async contract-based authority\ntype ContractAuthority struct {\n\tcontractPath    string\n\tcontractAddr    address\n\tcontractHandler PrivilegedActionHandler\n\tproposer        Authority // controls who can create proposals\n}\n\n// NewContractAuthority creates a new contract-based authority.\n//\n// SECURITY (Class-4 captured callback): `handler` is a caller-supplied\n// closure that runs SYNCHRONOUSLY inside Authorize, with the consumer's\n// authority. A hostile handler can swallow actions, log the caller, or\n// execute arbitrary code under the consumer's frame. The package-internal\n// wrappedAction guards \"execute action only from contractAddr\" but the\n// handler can call wrappedAction however it likes (multiple times, never,\n// out of order). Register only trusted handler functions; treat handler\n// registration as the trust boundary.\nfunc NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {\n\treturn \u0026ContractAuthority{\n\t\tcontractPath:    path,\n\t\tcontractAddr:    chain.PackageAddress(path),\n\t\tcontractHandler: handler,\n\t\tproposer:        NewAutoAcceptAuthority(), // default: anyone can propose\n\t}\n}\n\n// NewRestrictedContractAuthority creates a new contract authority with a\n// proposer restriction.\n//\n// SECURITY:\n//   - `handler` is the same Class-4 captured-callback risk as\n//     NewContractAuthority — runs synchronously inside Authorize with the\n//     consumer's authority. Register only trusted handler functions.\n//   - `proposer` is an open-interface input (Class-3 impl-substitution).\n//     A hostile proposer Authority can always-approve creation of any\n//     proposal, defeating the restriction. Pass canonical impls only.\nfunc NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority {\n\tif path == \"\" {\n\t\tpanic(\"contract path cannot be empty\")\n\t}\n\tif handler == nil {\n\t\tpanic(\"contract handler cannot be nil\")\n\t}\n\tif proposer == nil {\n\t\tpanic(\"proposer cannot be nil\")\n\t}\n\treturn \u0026ContractAuthority{\n\t\tcontractPath:    path,\n\t\tcontractAddr:    chain.PackageAddress(path),\n\t\tcontractHandler: handler,\n\t\tproposer:        proposer,\n\t}\n}\n\nfunc (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\tif a.contractHandler == nil {\n\t\treturn errors.New(\"contract handler is not set\")\n\t}\n\n\t// setup a once instance to ensure the action is executed only once\n\texecutionOnce := once.Once{}\n\n\t// wrappedAction enforces at-most-once invocation. The previous\n\t// gate `unsafe.CurrentRealm() == contractAddr` is removed: it\n\t// was .Title()-bypassable (runtime.CurrentRealm walks past\n\t// non-crossing frames to the most-recent crossing ancestor) and\n\t// the trust boundary is now upstream — Authorizer.DoByCurrent /\n\t// DoByPrevious require rlm.IsCurrent() and pass a non-forgeable\n\t// principal to Authorize, while the consumer realm's handler\n\t// closure is the Class-4 trust root by lexical capture at\n\t// registration time.\n\twrappedAction := func() error {\n\t\treturn executionOnce.DoErr(func() error {\n\t\t\treturn action()\n\t\t})\n\t}\n\n\t// Use the proposer authority to control who can create proposals\n\treturn a.proposer.Authorize(caller, title+\"_proposal\", func() error {\n\t\tif err := a.contractHandler(title, wrappedAction); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}, args...)\n}\n\nfunc (a *ContractAuthority) String() string {\n\treturn ufmt.Sprintf(\"contract_authority[contract=%s]\", a.contractPath)\n}\n\n// AutoAcceptAuthority implements an authority that accepts all actions\n// AutoAcceptAuthority is a simple authority that automatically accepts all\n// actions.\n// It can be used as a proposer authority to allow anyone to create proposals.\ntype AutoAcceptAuthority struct{}\n\nfunc NewAutoAcceptAuthority() *AutoAcceptAuthority {\n\treturn \u0026AutoAcceptAuthority{}\n}\n\nfunc (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\treturn action()\n}\n\nfunc (a *AutoAcceptAuthority) String() string {\n\treturn \"auto_accept_authority\"\n}\n\n// droppedAuthority implements an authority that denies all actions\ntype droppedAuthority struct{}\n\nfunc NewDroppedAuthority() Authority {\n\treturn \u0026droppedAuthority{}\n}\n\nfunc (a *droppedAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\treturn errors.New(\"dropped authority: all actions are denied\")\n}\n\nfunc (a *droppedAuthority) String() string {\n\treturn \"dropped_authority\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/authz\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"boards","path":"gno.land/r/archive/boards","files":[{"name":"board.gno","body":"package boards\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n//----------------------------------------\n// Board\n\ntype BoardID uint64\n\nfunc (bid BoardID) String() string {\n\treturn strconv.Itoa(int(bid))\n}\n\ntype Board struct {\n\tid        BoardID // only set for public boards.\n\turl       string\n\tname      string\n\tcreator   address\n\tthreads   avl.Tree // Post.id -\u003e *Post\n\tpostsCtr  uint64   // increments Post.id\n\tcreatedAt time.Time\n\tdeleted   avl.Tree // TODO reserved for fast-delete.\n}\n\nfunc newBoard(id BoardID, url string, name string, creator address) *Board {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\texists := gBoardsByName.Has(name)\n\tif exists {\n\t\tpanic(\"board already exists\")\n\t}\n\treturn \u0026Board{\n\t\tid:        id,\n\t\turl:       url,\n\t\tname:      name,\n\t\tcreator:   creator,\n\t\tthreads:   avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t\tdeleted:   avl.Tree{},\n\t}\n}\n\n/* TODO support this once we figure out how to ensure URL correctness.\n// A private board is not tracked by gBoards*,\n// but must be persisted by the caller's realm.\n// Private boards have 0 id and does not ping\n// back the remote board on reposts.\nfunc NewPrivateBoard(_ realm, url string, name string, creator address) *Board {\n\treturn newBoard(0, url, name, creator)\n}\n*/\n\nfunc (board *Board) IsPrivate() bool {\n\treturn board.id == 0\n}\n\nfunc (board *Board) GetThread(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpostI := board.threads.Get(pidkey)\n\tif postI == nil {\n\t\treturn nil\n\t}\n\treturn postI.(*Post)\n}\n\nfunc (board *Board) AddThread(creator address, title string, body string) *Post {\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\tthread := newPost(board, pid, creator, title, body, pid, 0, 0)\n\tboard.threads.Set(pidkey, thread)\n\treturn thread\n}\n\n// NOTE: this can be potentially very expensive for threads with many replies.\n// TODO: implement optional fast-delete where thread is simply moved.\nfunc (board *Board) DeleteThread(pid PostID) {\n\tpidkey := postIDKey(pid)\n\t_, removed := board.threads.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"thread does not exist with id \" + pid.String())\n\t}\n}\n\nfunc (board *Board) HasPermission(addr address, perm Permission) bool {\n\tif board.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// Renders the board for display suitable as plaintext in\n// console.  This is suitable for demonstration or tests,\n// but not for prod.\nfunc (board *Board) RenderBoard() string {\n\tstr := \"\"\n\tstr += \"\\\\[[post](\" + board.GetPostFormURL() + \")]\\n\\n\"\n\tif board.threads.Size() \u003e 0 {\n\t\tboard.threads.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tif str != \"\" {\n\t\t\t\tstr += \"----------------------------------------\\n\"\n\t\t\t}\n\t\t\tstr += value.(*Post).RenderSummary() + \"\\n\"\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\nfunc (board *Board) incGetPostID() PostID {\n\tboard.postsCtr++\n\treturn PostID(board.postsCtr)\n}\n\nfunc (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string {\n\tif replyID == 0 {\n\t\treturn board.url + \"/\" + threadID.String()\n\t} else {\n\t\treturn board.url + \"/\" + threadID.String() + \"/\" + replyID.String()\n\t}\n}\n\nfunc (board *Board) GetPostFormURL() string {\n\treturn gRealmLink.Call(\"CreateThread\", \"bid\", board.id.String())\n}\n"},{"name":"boards.gno","body":"package boards\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgRealmLink      = txlink.Realm(\"gno.land/r/archive/boards\")\n\tgBoards         avl.Tree    // id -\u003e *Board\n\tgBoardsCtr      int         // increments Board.id\n\tgBoardsByName   avl.Tree    // name -\u003e *Board\n\tgDefaultAnonFee = 100000000 // minimum fee required if anonymous\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n"},{"name":"example_post.md","body":"Hey all! 👋\n\nThis is my first post in this land!\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/boards\"\ngno = \"0.9\"\n"},{"name":"misc.gno","body":"package boards\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getBoard(bid BoardID) *Board {\n\tbidkey := boardIDKey(bid)\n\tboard_ := gBoards.Get(bidkey)\n\tif board_ == nil {\n\t\treturn nil\n\t}\n\tboard := board_.(*Board)\n\treturn board\n}\n\nfunc incGetBoardID() BoardID {\n\tgBoardsCtr++\n\treturn BoardID(gBoardsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\" \", length-len(str)) + str\n\t}\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc boardIDKey(bid BoardID) string {\n\treturn padZero(uint64(bid), 10)\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/u/\" + addr.String() + \")\"\n\t} else {\n\t\treturn \"[@\" + user.Name() + \"](/u/\" + user.Name() + \")\"\n\t}\n}\n\nfunc usernameOf(addr address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t}\n\treturn user.Name()\n}\n"},{"name":"post.gno","body":"package boards\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n//----------------------------------------\n// Post\n\n// NOTE: a PostID is relative to the board.\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\n// A Post is a \"thread\" or a \"reply\" depending on context.\n// A thread is a Post of a Board that holds other replies.\ntype Post struct {\n\tboard       *Board\n\tid          PostID\n\tcreator     address\n\ttitle       string // optional\n\tbody        string\n\treplies     avl.Tree // Post.id -\u003e *Post\n\trepliesAll  avl.Tree // Post.id -\u003e *Post (all replies, for top-level posts)\n\treposts     avl.Tree // Board.id -\u003e Post.id\n\tthreadID    PostID   // original Post.id\n\tparentID    PostID   // parent Post.id (if reply or repost)\n\trepostBoard BoardID  // original Board.id (if repost)\n\tcreatedAt   time.Time\n\tupdatedAt   time.Time\n}\n\nfunc newPost(board *Board, id PostID, creator address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {\n\treturn \u0026Post{\n\t\tboard:       board,\n\t\tid:          id,\n\t\tcreator:     creator,\n\t\ttitle:       title,\n\t\tbody:        body,\n\t\treplies:     avl.Tree{},\n\t\trepliesAll:  avl.Tree{},\n\t\treposts:     avl.Tree{},\n\t\tthreadID:    threadID,\n\t\tparentID:    parentID,\n\t\trepostBoard: repostBoard,\n\t\tcreatedAt:   time.Now(),\n\t}\n}\n\nfunc (post *Post) IsThread() bool {\n\treturn post.parentID == 0\n}\n\nfunc (post *Post) GetPostID() PostID {\n\treturn post.id\n}\n\nfunc (post *Post) AddReply(creator address, body string) *Post {\n\tboard := post.board\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\treply := newPost(board, pid, creator, \"\", body, post.threadID, post.id, 0)\n\tpost.replies.Set(pidkey, reply)\n\tif post.threadID == post.id {\n\t\tpost.repliesAll.Set(pidkey, reply)\n\t} else {\n\t\tthread := board.GetThread(post.threadID)\n\t\tthread.repliesAll.Set(pidkey, reply)\n\t}\n\treturn reply\n}\n\nfunc (post *Post) Update(title string, body string) {\n\tpost.title = title\n\tpost.body = body\n\tpost.updatedAt = time.Now()\n}\n\nfunc (thread *Post) GetReply(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\treplyI := thread.repliesAll.Get(pidkey)\n\tif replyI == nil {\n\t\treturn nil\n\t} else {\n\t\treturn replyI.(*Post)\n\t}\n}\n\nfunc (post *Post) AddRepostTo(creator address, title, body string, dst *Board) *Post {\n\tif !post.IsThread() {\n\t\tpanic(\"cannot repost non-thread post\")\n\t}\n\tpid := dst.incGetPostID()\n\tpidkey := postIDKey(pid)\n\trepost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)\n\tdst.threads.Set(pidkey, repost)\n\tif !dst.IsPrivate() {\n\t\tbidkey := boardIDKey(dst.id)\n\t\tpost.reposts.Set(bidkey, pid)\n\t}\n\treturn repost\n}\n\nfunc (thread *Post) DeletePost(pid PostID) {\n\tif thread.id == pid {\n\t\tpanic(\"should not happen\")\n\t}\n\tpidkey := postIDKey(pid)\n\tpostI, removed := thread.repliesAll.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"post not found in thread\")\n\t}\n\tpost := postI.(*Post)\n\tif post.parentID != thread.id {\n\t\tparent := thread.GetReply(post.parentID)\n\t\tparent.replies.Remove(pidkey)\n\t} else {\n\t\tthread.replies.Remove(pidkey)\n\t}\n}\n\nfunc (post *Post) HasPermission(addr address, perm Permission) bool {\n\tif post.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\t// post notes inherit permissions of the board.\n\treturn post.board.HasPermission(addr, perm)\n}\n\nfunc (post *Post) GetSummary() string {\n\treturn summaryOf(post.body, 80)\n}\n\nfunc (post *Post) GetURL() string {\n\tif post.IsThread() {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.id, 0)\n\t} else {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.threadID, post.id)\n\t}\n}\n\nfunc (post *Post) GetReplyFormURL() string {\n\treturn gRealmLink.Call(\"CreateReply\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetRepostFormURL() string {\n\treturn gRealmLink.Call(\"CreateRepost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetDeleteFormURL() string {\n\treturn gRealmLink.Call(\"DeletePost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) RenderSummary() string {\n\tif post.repostBoard != 0 {\n\t\tdstBoard := getBoard(post.repostBoard)\n\t\tif dstBoard == nil {\n\t\t\tpanic(\"repostBoard does not exist\")\n\t\t}\n\t\tthread := dstBoard.GetThread(PostID(post.parentID))\n\t\tif thread == nil {\n\t\t\treturn \"reposted post does not exist\"\n\t\t}\n\t\treturn \"Repost: \" + post.GetSummary() + \"\\n\" + thread.RenderSummary()\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += \"## [\" + summaryOf(post.title, 80) + \"](\" + post.GetURL() + \")\\n\"\n\t\tstr += \"\\n\"\n\t}\n\tstr += post.GetSummary() + \"\\n\"\n\tstr += \"\\\\- \" + displayAddressMD(post.creator) + \",\"\n\tstr += \" [\" + post.createdAt.Format(\"2006-01-02 3:04pm MST\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\"\n\tstr += \" (\" + strconv.Itoa(post.replies.Size()) + \" replies)\"\n\tstr += \" (\" + strconv.Itoa(post.reposts.Size()) + \" reposts)\" + \"\\n\"\n\treturn str\n}\n\nfunc (post *Post) RenderPost(indent string, levels int) string {\n\tif post == nil {\n\t\treturn \"nil post\"\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += indent + \"# \" + post.title + \"\\n\"\n\t\tstr += indent + \"\\n\"\n\t}\n\tstr += indentBody(indent, post.body) + \"\\n\" // TODO: indent body lines.\n\tstr += indent + \"\\\\- \" + displayAddressMD(post.creator) + \", \"\n\tstr += \"[\" + post.createdAt.Format(\"2006-01-02 3:04pm (MST)\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[reply](\" + post.GetReplyFormURL() + \")]\"\n\tif post.IsThread() {\n\t\tstr += \" \\\\[[repost](\" + post.GetRepostFormURL() + \")]\"\n\t}\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\\n\"\n\tif levels \u003e 0 {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tpost.replies.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\tstr += indent + \"\\n\"\n\t\t\t\tstr += value.(*Post).RenderPost(indent+\"\u003e \", levels-1)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t} else {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tstr += indent + \"\\n\"\n\t\t\tstr += indent + \"_[see all \" + strconv.Itoa(post.replies.Size()) + \" replies](\" + post.GetURL() + \")_\\n\"\n\t\t}\n\t}\n\treturn str\n}\n\n// render reply and link to context thread\nfunc (post *Post) RenderInner() string {\n\tif post.IsThread() {\n\t\tpanic(\"unexpected thread\")\n\t}\n\tthreadID := post.threadID\n\t// replyID := post.id\n\tparentID := post.parentID\n\tstr := \"\"\n\tstr += \"_[see thread](\" + post.board.GetURLFromThreadAndReplyID(\n\t\tthreadID, 0) + \")_\\n\\n\"\n\tthread := post.board.GetThread(post.threadID)\n\tvar parent *Post\n\tif thread.id == parentID {\n\t\tparent = thread\n\t} else {\n\t\tparent = thread.GetReply(parentID)\n\t}\n\tstr += parent.RenderPost(\"\", 0)\n\tstr += \"\\n\"\n\tstr += post.RenderPost(\"\u003e \", 5)\n\treturn str\n}\n"},{"name":"public.gno","body":"package boards\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetBoardIDFromName(name string) (BoardID, bool) {\n\tboardI := gBoardsByName.Get(name)\n\tif boardI == nil {\n\t\treturn 0, false\n\t}\n\treturn boardI.(*Board).id, true\n}\n\nfunc CreateBoard(cur realm, name string) BoardID {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tbid := incGetBoardID()\n\tcaller := unsafe.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tpanic(\"unauthorized\")\n\t}\n\turl := \"/r/archive/boards:\" + name\n\tboard := newBoard(bid, url, name, caller)\n\tbidkey := boardIDKey(bid)\n\tgBoards.Set(bidkey, board)\n\tgBoardsByName.Set(name, board)\n\treturn board.id\n}\n\n// checkAnonFee reads unsafe.OriginSend() to verify the anonymous-posting\n// fee was attached to the tx. Callers MUST also assert\n// cur.Previous().IsUserCall() before calling — see\n// docs/resources/effective-gno.md#verifying-inbound-coin-payments.\nfunc checkAnonFee() bool {\n\tsent := unsafe.OriginSend()\n\tanonFeeCoin := chain.NewCoin(\"ugnot\", int64(gDefaultAnonFee))\n\tif len(sent) == 1 \u0026\u0026 sent[0].IsGTE(anonFeeCoin) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc CreateThread(cur realm, bid BoardID, title string, body string) PostID {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.AddThread(caller, title, body)\n\treturn thread.id\n}\n\nfunc CreateReply(cur realm, bid BoardID, threadid, postid PostID, body string) PostID {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\treply := thread.AddReply(caller, body)\n\t\treturn reply.id\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\treply := post.AddReply(caller, body)\n\t\treturn reply.id\n\t}\n}\n\n// If dstBoard is private, does not ping back.\n// If board specified by bid is private, panics.\nfunc CreateRepost(cur realm, bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\t// TODO: allow with gDefaultAnonFee payment.\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"src board not exist\")\n\t}\n\tif board.IsPrivate() {\n\t\tpanic(\"cannot repost from a private board\")\n\t}\n\tdst := getBoard(dstBoardID)\n\tif dst == nil {\n\t\tpanic(\"dst board not exist\")\n\t}\n\tthread := board.GetThread(postid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\trepost := thread.AddRepostTo(caller, title, body, dst)\n\treturn repost.id\n}\n\nfunc DeletePost(cur realm, bid BoardID, threadid, postid PostID, reason string) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// delete thread\n\t\tif !thread.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tboard.DeleteThread(threadid)\n\t} else {\n\t\t// delete thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.DeletePost(postid)\n\t}\n}\n\nfunc EditPost(cur realm, bid BoardID, threadid, postid PostID, title, body string) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tcaller := unsafe.OriginCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// edit thread\n\t\tif !thread.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.Update(title, body)\n\t} else {\n\t\t// edit thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tpost.Update(title, body)\n\t}\n}\n"},{"name":"render.gno","body":"package boards\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderBoard(bid BoardID) string {\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\treturn \"missing board\"\n\t}\n\treturn board.RenderBoard()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"These are all the boards of this realm:\\n\\n\"\n\t\tgBoards.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tboard := value.(*Board)\n\t\t\tstr += \" * [\" + board.url + \"](\" + board.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/archive/boards:BOARD_NAME\n\t\tname := parts[0]\n\t\tboardI := gBoardsByName.Get(name)\n\t\tif boardI == nil {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\treturn boardI.(*Board).RenderBoard()\n\t} else if len(parts) == 2 {\n\t\t// /r/archive/boards:BOARD_NAME/THREAD_ID\n\t\tname := parts[0]\n\t\tboardI := gBoardsByName.Get(name)\n\t\tif boardI == nil {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\treturn thread.RenderPost(\"\", 5)\n\t} else if len(parts) == 3 {\n\t\t// /r/archive/boards:BOARD_NAME/THREAD_ID/REPLY_ID\n\t\tname := parts[0]\n\t\tboardI := gBoardsByName.Get(name)\n\t\tif boardI == nil {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\trid, err := strconv.Atoi(parts[2])\n\t\tif err != nil {\n\t\t\treturn \"invalid reply id: \" + parts[2]\n\t\t}\n\t\treply := thread.GetReply(PostID(rid))\n\t\tif reply == nil {\n\t\t\treturn \"reply does not exist with id: \" + parts[2]\n\t\t}\n\t\treturn reply.RenderInner()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n"},{"name":"role.gno","body":"package boards\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission   Permission = \"role:edit\"\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"txfees","path":"gno.land/r/sys/txfees","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/txfees\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"render.gno","body":"package txfees\n\nimport (\n\t\"chain/banker\"\n\t\"strings\"\n)\n\nfunc Render(cur realm, _ string) string {\n\tbanker_ := banker.NewReadonlyBanker()\n\trealmAddr := cur.Address()\n\tbalance := banker_.GetCoins(realmAddr).String()\n\n\tif strings.TrimSpace(balance) == \"\" {\n\t\tbalance = \"\\\\\u003cempty\\\\\u003e\"\n\t}\n\n\tvar output string\n\toutput += \"# Transaction Fees\\n\"\n\toutput += \"Balance: \" + balance + \"\\n\\n\"\n\n\toutput += \"Bucket address: \" + realmAddr.String() + \"\\n\"\n\treturn output\n}\n"},{"name":"txfees.gno","body":"package txfees\n\n// XXX: TODO distribution logic\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"crossrealm_d","path":"gno.land/r/tests/vm/crossrealm_d","files":[{"name":"crossrealm.gno","body":"package crossrealm_d\n\n// Simple stateful realm for cross-realm consistency tests.\n// Separated from crossrealm_b to avoid perturbing its object IDs.\n//\n// Contains both crossing and non-crossing setters so tests can\n// demonstrate that non-crossing calls from another realm cannot\n// silently mutate state via assign+recover.\n\nvar counter int\n\nfunc init() {\n\tcounter = 100\n}\n\n// SetCounter: non-crossing. Calling this cross-realm triggers\n// the readonly check because it directly assigns a package var.\nfunc SetCounter(n int) {\n\tcounter = n\n}\n\n// SetCounterCrossing: crossing version. This works correctly\n// cross-realm because the caller enters this realm's context.\nfunc SetCounterCrossing(cur realm, n int) {\n\tcounter = n\n}\n\nfunc GetCounter(cur realm) int {\n\treturn counter\n}\n\n// DoubleCounter reads counter and doubles it. Used to show that\n// if counter were silently corrupted in memory, subsequent crossing\n// calls would act on the wrong value.\nfunc DoubleCounter(cur realm) int {\n\tcounter = counter * 2\n\treturn counter\n}\n\n// MutateBytes mutates the first byte of bz to 0xff. Used to test\n// whether a foreign realm can write to caller-allocated bytes.\n// Under storage=authority, bz has PkgID=caller; borrow rule #1 here\n// makes m.Realm=crossrealm_d (declaring realm); the write site\n// (bz[0] = ...) should fire readonly because bz.PkgID != m.Realm.ID.\nfunc MutateBytes(bz []byte) {\n\tbz[0] = 0xff\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/crossrealm_d\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"launderpkg","path":"gno.land/p/demo/tests/launderpkg","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/launderpkg\"\ngno = \"0.9\"\n"},{"name":"launderpkg.gno","body":"// Package launderpkg defines a struct type that the laundervictim\n// realm uses as the type of its package-level state. The Set method\n// is the \"innocent /p/-helper\" that an attacker tries to weaponize\n// via the receiver-borrow rule (PushFrameCall borrow rule 2):\n// when the receiver is owned by /r/X, calling Set borrows m.Realm to\n// /r/X, and the write inside Set runs with /r/X authority.\npackage launderpkg\n\ntype Object struct {\n\tField string\n}\n\n// PInitData is a /p/-init-allocated package-level data var. Its\n// StructValue carries ObjectInfo.PkgID = /p/demo/tests/launderpkg.\n// Used by zrealm_launder_pdata_* filetests to probe the /p/-source\n// read patterns that the existing 62 /r/-source launder filetests\n// don't cover. Empirically, direct value-read and pointer-deref of\n// PInitData from a /r/-caller both panic readonly tainted — the\n// /p/-source bytes are not adoptable into /r/-authority via these\n// patterns.\nvar PInitData = Object{Field: \"p-init\"}\n\n// Set is the canonical \"/p/ helper that mutates /r/-owned state\"\n// pattern. Looks benign — like list.Set, avl.Set, etc. — but if a\n// foreign caller obtains a pointer to a victim realm's Object, they\n// can invoke Set on it and the borrow rule grants them victim\n// authority for the call. Exposing a *Object out of a realm is\n// equivalent to consenting to mutation by any caller that holds the\n// pointer.\nfunc (o *Object) Set(s string) {\n\to.Field = s\n}\n\n// Read is a read-only accessor.\nfunc (o *Object) Read() string {\n\treturn o.Field\n}\n\n// Mutator is an interface — callers can pass any implementation. A\n// realistic pattern: /p/orig defines a hook interface and exposes a\n// method that lets callers register an impl for some operation.\ntype Mutator interface {\n\tRun(*Immutable)\n}\n\n// UseMutator is a /p/-method on *Object that dispatches an interface\n// method (Mutator.Run) with target as the argument. This is the\n// realistic shape — a /p/-library invoking a user-supplied hook on\n// /r/-owned data.\nfunc (o *Object) UseMutator(target *Immutable, m Mutator) {\n\tm.Run(target)\n}\n\n// SliceMutator's Run takes a SLICE of pointers — exercises the\n// \"non-pointer parameter that still conveys writable foreign data\"\n// shape that Attack K explores.\ntype SliceMutator interface {\n\tRun([]*Immutable)\n}\n\n// UseSliceMutator dispatches a SliceMutator on a single-element\n// slice carrying target.\nfunc (o *Object) UseSliceMutator(target *Immutable, m SliceMutator) {\n\tm.Run([]*Immutable{target})\n}\n\n// AnyMutator's Run takes `any` — interface-typed parameter. The\n// caller can box a *Immutable into the interface and an attacker\n// impl can type-assert back to write. Tests whether the predicate\n// needs to treat interface-typed params as potentially foreign.\ntype AnyMutator interface {\n\tRun(any)\n}\n\n// UseAnyMutator boxes target into `any` before dispatching.\nfunc (o *Object) UseAnyMutator(target *Immutable, m AnyMutator) {\n\tm.Run(target)\n}\n\n// Bare is a /p/-declared struct type with NO methods. Used to test\n// whether embedding/fielding a methods-less /p/-type inside an\n// /r/-declared container exposes any laundering vector through\n// DIRECT field writes (no method dispatch, no Apply callback). The\n// expectation: readonly taint on the /r/-container propagates to\n// inner /p/-typed fields and direct writes panic.\ntype Bare struct {\n\tField string\n}\n\n// Immutable is a deliberately read-only /p/ type: same struct layout\n// as Object, but no mutator method. A realm using Immutable as the\n// type of an exposed field intends \"read-only API.\" The launder game\n// variation explores whether an attacker /p/ package can convert\n// *Immutable to a type with a mutator method declared elsewhere.\ntype Immutable struct {\n\tField string\n}\n\n// Read is a read-only accessor.\nfunc (i *Immutable) Read() string {\n\treturn i.Field\n}\n\n// Apply is a higher-order helper that hands the *Immutable to a\n// caller-supplied callback. The signature looks read-only (no\n// mutator method on *Immutable itself), but Apply hands out an\n// addressable pointer to victim-owned memory while m.Realm is\n// borrowed to the victim. A /p/-declared callback substituted by\n// the caller therefore runs with victim authority. This is the\n// avl.Tree.Iterate / list.ForEach shape — common in /p/ libraries.\nfunc (i *Immutable) Apply(fn func(*Immutable)) {\n\tfn(i)\n}\n\n// BumpToPwn is a no-arg, no-return /p/-method that mutates the\n// receiver. Used by stored-bound-method-value laundering probes:\n// `mv := victimImmPtr.BumpToPwn` has type `func()`, which fits\n// PlainHook = func(). When the bound method value is stored and\n// invoked later from /r/-victim context, recv-borrow borrow rule #2 fires on\n// the /r/-victim-stamped recv → m.Realm = /r/-victim → write\n// commits.\nfunc (i *Immutable) BumpToPwn() {\n\ti.Field = \"pwnd-via-bound-mv\"\n}\n\n// DeferApply is the defer-variant of Apply: schedules fn(i) as a\n// defer instead of calling synchronously. Used to probe whether\n// the borrow rules apply the same when a callback is invoked from\n// inside a /p/-function's defer queue.\nfunc (i *Immutable) DeferApply(fn func(*Immutable)) {\n\tdefer fn(i)\n}\n\n// PanicAfterApply: invokes fn(i) synchronously, then panics after\n// return. If fn ran without panicking (write succeeded), the panic\n// here is /p/-realm panic propagating up.\nfunc (i *Immutable) PanicAfterApply(fn func(*Immutable)) {\n\tfn(i)\n\tpanic(\"post-apply panic\")\n}\n\n// RecoverApply: defers a recover(), then calls fn(i). If fn panics\n// with readonly, recover catches it (within the same /p/-pkg\n// frame). Returns the recovered value.\nfunc (i *Immutable) RecoverApply(fn func(*Immutable)) (rec any) {\n\tdefer func() {\n\t\trec = recover()\n\t}()\n\tfn(i)\n\treturn\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"launderattack","path":"gno.land/p/demo/tests/launderattack","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/launderattack\"\ngno = \"0.9\"\n"},{"name":"launderattack.gno","body":"// Package launderattack imports launderpkg and provides functions\n// that attempt to mutate a /p/-typed value passed by pointer. This is\n// the \"/p/attack imports /p/orig\" variation: the goal is for code\n// declared in this /p/ package to mutate a /r/-victim's instance of\n// launderpkg.Object.\npackage launderattack\n\nimport \"gno.land/p/demo/tests/launderpkg\"\n\n// TamperDirect writes through the pointer in its own body. The body\n// runs under whatever m.Realm the caller had at PushFrameCall — for a\n// top-level /p/ function with no receiver, that's the caller's realm.\n// If the caller is attacker-realm, m.Realm at the write is\n// attacker-realm and the readonly check fires on a foreign-stamped\n// base.\nfunc TamperDirect(o *launderpkg.Object, s string) {\n\to.Field = s\n}\n\n// TamperViaMethod dispatches through the launderpkg.Object's own /p/\n// method. PushFrameCall for Set sees a receiver stamped with the\n// victim's realm (the pointer aliases victim's persisted state),\n// triggering borrow rule 2: m.Realm becomes victim for the duration\n// of Set. The write succeeds — but this requires launderpkg.Object\n// to expose a Set method to begin with.\nfunc TamperViaMethod(o *launderpkg.Object, s string) {\n\to.Set(s)\n}\n\n// Tamper has the SAME underlying struct layout as launderpkg.Immutable.\n// /p/launderpkg deliberately gave Immutable no mutator method —\n// callers were supposed to be unable to write its Field. /p/attack\n// declares its own type with the same layout and adds a mutator. An\n// attacker that holds a *launderpkg.Immutable can convert it to\n// *Tamper and invoke Tamper.Set — the conversion is purely a\n// type-tag change, the pointer still aliases victim's persisted\n// memory. PushFrameCall for Tamper.Set then sees recv stamped with\n// victim's realm (the underlying object is unchanged) and\n// borrow-routes m.Realm to victim. The write succeeds.\ntype Tamper struct {\n\tField string\n}\n\n// Set is the mutator that launderpkg.Immutable deliberately did NOT\n// expose. /p/attack adds it via the parallel-type trick.\nfunc (t *Tamper) Set(s string) {\n\tt.Field = s\n}\n\n// Convert is the attacker's helper that does the type punning so the\n// caller doesn't have to write the conversion inline.\nfunc Convert(p *launderpkg.Immutable) *Tamper {\n\treturn (*Tamper)(p)\n}\n\n// EvilMutator implements launderpkg.Mutator with a PRIMITIVE\n// underlying type. Underlying-type matters: a struct/array/etc.\n// receiver has a *StructValue that gets PkgID-stamped at allocation,\n// triggering PushFrameCall's receiver-borrow rule to shift m.Realm\n// back to /p/launderattack. A primitive-underlying type has no\n// *StructValue and no PkgID — `recv.GetFirstObject` returns nil, so\n// the borrow rule's `if obj != nil { ... }` branch is skipped and\n// m.Realm stays at whatever the caller had it set to.\n//\n// When Run is dispatched via interface from inside a /p/-method\n// body that was receiver-borrowed to the victim, m.Realm at Run's\n// entry is the victim's — and stays the victim's, because EvilMutator\n// (an int underneath) carries no PkgID to borrow against. The write\n// inside Run commits under victim authority.\ntype EvilMutator int\n\nfunc (EvilMutator) Run(i *launderpkg.Immutable) {\n\ti.Field = \"pwnd-via-iface\"\n}\n\n// EvilNilRecv tests the nil-pointer-receiver variant. Calling a method\n// on a nil *EvilNilRecv is legal in Gno when the body doesn't deref\n// the receiver. recv = PointerValue{Base: nil} → GetBase returns nil\n// → GetFirstObject returns nil. Same \"no anchor\" gap as the\n// primitive-receiver case, reachable through *T receivers.\ntype EvilNilRecv struct {\n\tX int // unused\n}\n\nfunc (n *EvilNilRecv) Run(i *launderpkg.Immutable) {\n\t// Does NOT dereference n. Just writes through target.\n\ti.Field = \"pwnd-via-nilrecv\"\n}\n\n// EvilFunc / EvilSlice / EvilMap — additional nil-anchor shapes.\n// nil-valued receivers of defined types whose underlying is a\n// reference type (slice/map/func) also have GetFirstObject == nil.\n// The Attack H/I fix should cover all of them via the\n// recvDeclaredTypePkgPath helper.\ntype EvilFunc func()\n\nfunc (EvilFunc) Run(i *launderpkg.Immutable) { i.Field = \"pwnd-via-func\" }\n\ntype EvilSlice []int\n\nfunc (EvilSlice) Run(i *launderpkg.Immutable) { i.Field = \"pwnd-via-slice\" }\n\ntype EvilMap map[string]int\n\nfunc (EvilMap) Run(i *launderpkg.Immutable) { i.Field = \"pwnd-via-map\" }\n\n// EvilSliceMutator's Run takes a slice — NOT a pointer parameter, so\n// the Attack H/I fix's `hasForeignPPtrParam` predicate skips it.\n// Tests whether the anchor predicate needs to look INSIDE composite\n// parameter types for foreign-/p/ pointers.\ntype EvilSliceMutator int\n\nfunc (EvilSliceMutator) Run(s []*launderpkg.Immutable) {\n\ts[0].Field = \"pwnd-via-slice-arg\"\n}\n\n// EvilAnyMutator's Run takes `any` and type-asserts to *Immutable.\n// The signature reveals NO foreign-/p/-pointer statically — the\n// pointer is hidden inside the interface box. Probes whether the\n// predicate needs to fire on interface-typed params too.\ntype EvilAnyMutator int\n\nfunc (EvilAnyMutator) Run(x any) {\n\tt := x.(*launderpkg.Immutable)\n\tt.Field = \"pwnd-via-any-arg\"\n}\n\n// EvilWrite is a top-level /p/-declared function value matching\n// `func(*launderpkg.Immutable)`. Top-level /p/ functions trigger\n// neither borrow rule #1 (not /r/-declared) nor borrow rule #2 (no receiver), so\n// when EvilWrite is invoked as a callback from inside a\n// borrowed-to-victim /p/-method body (e.g. Immutable.Apply, or any\n// avl.Tree.Iterate-style hook), it inherits the victim's m.Realm\n// and the write commits under victim authority.\nfunc EvilWrite(i *launderpkg.Immutable) {\n\ti.Field = \"pwnd-via-apply\"\n}\n\n// EvilObjectWrite is the same shape but for *Object — used by tests\n// that exercise Object's mutator surface through Apply-style callbacks.\nfunc EvilObjectWrite(o *launderpkg.Object) {\n\to.Field = \"pwnd-via-apply-obj\"\n}\n\n// StoredHook is a /p/attack package-level closure: a FuncLit evaluated\n// during /p/launderattack init, so its ObjectInfo.PkgID is stamped\n// /p/launderattack. Unlike a top-level FuncDecl (EvilWrite/EvilObjectWrite,\n// IsClosure=false), invoking a closure triggers PushFrameCall's borrow rule\n// #3, which borrows m.Realm to the closure's construction realm\n// (/p/launderattack). Used to probe rule #3: a write through it to a foreign\n// /r/ object must still be rejected. (If rule #3 ever regressed to a nil\n// borrow, m.Realm would go nil and the write would silently succeed.)\nvar StoredHook = func(o *launderpkg.Object) {\n\to.Field = \"pwnd-via-stored-closure\"\n}\n\n// Tamperer is a stamped /p/attack value (constructed at init under\n// /p/attack's realm context, stamped /p/attack, persisted Frozen).\n// Methods on Tamperer get receiver-borrowed to /p/attack at call\n// time — useful as a control to compare against attacks where the\n// receiver is victim-stamped.\ntype Tamperer struct{}\n\n// TamperMethod is a /p/attack-defined method. When the caller invokes\n// Tamperer{}.TamperMethod(o, s), the receiver Tamperer{} is\n// constructed in the caller's realm, so receiver-borrow lands at the\n// caller's realm, not /p/attack — the receiver carries the caller's\n// stamp, not /p/attack's.\nfunc (Tamperer) TamperMethod(o *launderpkg.Object, s string) {\n\to.Field = s\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"boards","path":"gno.land/p/gnoland/boards","files":[{"name":"board.gno","body":"package boards\n\nimport \"time\"\n\n// Board defines a type for boards.\ntype Board struct {\n\t// ID is the unique identifier of the board.\n\tID ID\n\n\t// Name is the current name of the board.\n\tName string\n\n\t// Aliases contains a list of alternative names for the board.\n\tAliases []string\n\n\t// Readonly indicates that the board is readonly.\n\tReadonly bool\n\n\t// Threads contains board threads.\n\tThreads PostStorage\n\n\t// ThreadsSequence generates sequential ID for new threads.\n\tThreadsSequence IdentifierGenerator\n\n\t// Permissions enables support for permissioned boards.\n\t// This type of boards allows managing members with roles and permissions.\n\t// It also enables the implementation of permissioned execution of board related features.\n\tPermissions Permissions\n\n\t// Creator is the account address that created the board.\n\tCreator address\n\n\t// Meta allows storing board metadata.\n\tMeta any\n\n\t// CreatedAt is the board's creation time.\n\tCreatedAt time.Time\n\n\t// UpdatedAt is the board's update time.\n\tUpdatedAt time.Time\n}\n\n// New creates a new basic non permissioned board.\nfunc New(id ID) *Board {\n\treturn \u0026Board{\n\t\tID:              id,\n\t\tThreads:         NewPostStorage(),\n\t\tThreadsSequence: NewIdentifierGenerator(),\n\t\tCreatedAt:       time.Now(),\n\t}\n}\n\n// SetID sets board ID value.\nfunc (board *Board) SetID(v ID) {\n\tboard.ID = v\n}\n\n// SetName sets name value.\nfunc (board *Board) SetName(v string) {\n\tboard.Name = v\n}\n\n// SetAliases sets board name aliases.\nfunc (board *Board) SetAliases(v []string) {\n\tboard.Aliases = v\n}\n\n// SetReadonly sets readonly value.\nfunc (board *Board) SetReadonly(v bool) {\n\tboard.Readonly = v\n}\n\n// SetThreadStorage sets the storage where board threads are stored.\nfunc (board *Board) SetThreadStorage(v PostStorage) {\n\tboard.Threads = v\n}\n\n// SetThreadsSequence sets the sequential thread ID generator.\nfunc (board *Board) SetThreadsSequence(v IdentifierGenerator) {\n\tboard.ThreadsSequence = v\n}\n\n// SetPermissions sets permissions value.\nfunc (board *Board) SetPermissions(v Permissions) {\n\tboard.Permissions = v\n}\n\n// SetCreator sets the address of the account that created the board.\nfunc (board *Board) SetCreator(v address) {\n\tboard.Creator = v\n}\n\n// SetCreatedAt sets the time when board was created.\nfunc (board *Board) SetCreatedAt(v time.Time) {\n\tboard.CreatedAt = v\n}\n\n// SetUpdatedAt sets the time when a board value was updated.\nfunc (board *Board) SetUpdatedAt(v time.Time) {\n\tboard.UpdatedAt = v\n}\n\n// SetMeta sets board metadata.\nfunc (board *Board) SetMeta(v any) {\n\tboard.Meta = v\n}\n"},{"name":"flag_storage.gno","body":"package boards\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype (\n\t// Flag defines a type for post flags\n\tFlag struct {\n\t\t// User is the user that flagged the post.\n\t\tUser address\n\n\t\t// Reason is the reason that describes why post is flagged.\n\t\tReason string\n\t}\n\n\t// FlagIterFn defines a function type to iterate post flags.\n\tFlagIterFn func(Flag) bool\n\n\t// FlagStorage defines an interface for storing posts flagging information.\n\tFlagStorage interface {\n\t\t// Exists checks if a flag from a user exists\n\t\tExists(address) bool\n\n\t\t// Add adds a new flag from a user.\n\t\tAdd(Flag) error\n\n\t\t// Remove removes a user flag.\n\t\tRemove(address) (removed bool)\n\n\t\t// Size returns the number of flags in the storage.\n\t\tSize() int\n\n\t\t// Iterate iterates post flags.\n\t\t// To reverse iterate flags use a negative count.\n\t\t// If the callback returns true, the iteration is stopped.\n\t\tIterate(start, count int, fn FlagIterFn) bool\n\t}\n)\n\n// NewFlagStorage creates a new storage for post flags.\n// The new storage uses an AVL tree to store flagging info.\nfunc NewFlagStorage() FlagStorage {\n\treturn \u0026flagStorage{bptree.NewBPTree32()}\n}\n\ntype flagStorage struct {\n\tflags *bptree.BPTree // address -\u003e string(reason)\n}\n\n// Exists checks if a flag from a user exists\nfunc (s flagStorage) Exists(addr address) bool {\n\treturn s.flags.Has(addr.String())\n}\n\n// Add adds a new flag from a user.\n// It fails if a flag from the same user exists.\nfunc (s *flagStorage) Add(f Flag) error {\n\tif !f.User.IsValid() {\n\t\treturn ufmt.Errorf(\"post flagging error, invalid user address: %s\", f.User)\n\t}\n\n\tk := f.User.String()\n\tif s.flags.Has(k) {\n\t\treturn ufmt.Errorf(\"flag from user already exists: %s\", f.User)\n\t}\n\n\ts.flags.Set(k, strings.TrimSpace(f.Reason))\n\treturn nil\n}\n\n// Remove removes a user flag.\nfunc (s *flagStorage) Remove(addr address) bool {\n\t_, removed := s.flags.Remove(addr.String())\n\treturn removed\n}\n\n// Size returns the number of flags in the storage.\nfunc (s flagStorage) Size() int {\n\treturn s.flags.Size()\n}\n\n// Iterate iterates post flags.\n// To reverse iterate flags use a negative count.\n// If the callback returns true, the iteration is stopped.\nfunc (s flagStorage) Iterate(start, count int, fn FlagIterFn) bool {\n\tif count \u003c 0 {\n\t\treturn s.flags.ReverseIterateByOffset(start, -count, func(k string, v any) bool {\n\t\t\treturn fn(Flag{\n\t\t\t\tUser:   address(k),\n\t\t\t\tReason: v.(string),\n\t\t\t})\n\t\t})\n\t}\n\n\treturn s.flags.IterateByOffset(start, count, func(k string, v any) bool {\n\t\treturn fn(Flag{\n\t\t\tUser:   address(k),\n\t\t\tReason: v.(string),\n\t\t})\n\t})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/gnoland/boards\"\ngno = \"0.9\"\n"},{"name":"id.gno","body":"package boards\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\nconst paddedStringLen = 10\n\n// ID defines a type for unique identifiers.\ntype ID uint64\n\n// String returns the ID as a string.\nfunc (id ID) String() string {\n\treturn strconv.FormatUint(uint64(id), 10)\n}\n\n// PaddedString returns the ID as a 10 character string padded with zeroes.\n// This value can be used for indexing by ID.\nfunc (id ID) PaddedString() string {\n\ts := id.String()\n\treturn strings.Repeat(\"0\", paddedStringLen-len(s)) + s\n}\n\n// Key returns the ID as a string which can be used to index by ID.\nfunc (id ID) Key() string {\n\treturn seqid.ID(id).String()\n}\n\n// IdentifierGenerator defines an interface for sequential unique identifier generators.\ntype IdentifierGenerator interface {\n\t// Current returns the last generated ID.\n\tLast() ID\n\n\t// Next generates a new ID or panics if increasing ID overflows.\n\tNext() ID\n}\n\n// NewIdentifierGenerator creates a new sequential unique identifier generator.\nfunc NewIdentifierGenerator() IdentifierGenerator {\n\treturn \u0026idGenerator{}\n}\n\ntype idGenerator struct {\n\tlast seqid.ID\n}\n\n// Current returns the last generated ID.\nfunc (g idGenerator) Last() ID {\n\treturn ID(g.last)\n}\n\n// Next generates a new ID or panics if increasing ID overflows.\nfunc (g *idGenerator) Next() ID {\n\treturn ID(g.last.Next())\n}\n"},{"name":"permission_set.gno","body":"package boards\n\n// PermissionSet defines a type to store any number of permissions.\ntype PermissionSet []uint64\n\n// NewPermissionSet creates a new PermissionSet containing the given permissions.\nfunc NewPermissionSet(perms ...Permission) PermissionSet {\n\tif len(perms) == 0 {\n\t\treturn nil\n\t}\n\n\t// Find max permission value to calculate slice size.\n\t// This allows any number of permissions to be assigned in any order.\n\tvar max Permission\n\tfor _, p := range perms {\n\t\tif p \u003e max {\n\t\t\tmax = p\n\t\t}\n\t}\n\n\ts := make(PermissionSet, int(max)/64+1)\n\tfor _, p := range perms {\n\t\t// Calculate the index within the set where the permission should be defined.\n\t\t// Each item in the set can contain 64 permissions, for example:\n\t\t// - Item 0: permissions 0 to 63\n\t\t// - Item 1: permissions 64 to 127\n\t\tidx := int(p) / 64\n\n\t\t// Turn on the bit that matches the permission, ranging from bit 0 to 63\n\t\ts[idx] |= 1 \u003c\u003c (uint(p) % 64)\n\t}\n\treturn s\n}\n\n// Has checks if a permission is in the set.\nfunc (s PermissionSet) Has(p Permission) bool {\n\tidx := int(p) / 64\n\tif idx \u003e= len(s) {\n\t\treturn false\n\t}\n\n\t// Check if the bit for the current permission is on\n\treturn s[idx]\u0026(1\u003c\u003c(uint(p)%64)) != 0\n}\n\n// IsEmpty reports whether the set contains no permissions.\nfunc (s PermissionSet) IsEmpty() bool {\n\tfor _, v := range s {\n\t\tif v != 0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"},{"name":"permissions.gno","body":"package boards\n\nimport \"strconv\"\n\ntype (\n\t// Role defines the type for user roles.\n\tRole string\n\n\t// Args is a list of generic arguments.\n\tArgs []interface{}\n\n\t// User contains user info.\n\tUser struct {\n\t\tAddress address\n\t\tRoles   []Role\n\t}\n\n\t// UsersIterFn defines a function type to iterate users.\n\tUsersIterFn func(User) bool\n\n\t// Permissions define an interface to for permissioned execution.\n\tPermissions interface {\n\t\t// HasRole checks if a user has a specific role assigned.\n\t\tHasRole(address, Role) bool\n\n\t\t// HasPermission checks if a user has a specific permission.\n\t\tHasPermission(address, Permission) bool\n\n\t\t// WithPermission calls a callback when a user has a specific permission.\n\t\t// It panics on error.\n\t\t//\n\t\t// An inline crossing function call can be used by the implementation if\n\t\t// crossing is required to update its internal state, for example to create\n\t\t// proposals that when approved execute the callback:\n\t\t//\n\t\t//  func(realm) {\n\t\t//    // Update internal realm state\n\t\t//    // ...\n\t\t//  }(cross)\n\t\tWithPermission(address, Permission, Args, func())\n\n\t\t// SetUserRoles adds a new user when it doesn't exist and sets its roles.\n\t\t// Method can also be called to change the roles of an existing user.\n\t\t// It panics on error.\n\t\tSetUserRoles(address, ...Role)\n\n\t\t// RemoveUser removes a user from the permissioner.\n\t\t// It panics on error.\n\t\tRemoveUser(address) (removed bool)\n\n\t\t// HasUser checks if a user exists.\n\t\tHasUser(address) bool\n\n\t\t// UsersCount returns the total number of users the permissioner contains.\n\t\tUsersCount() int\n\n\t\t// IterateUsers iterates permissions' users.\n\t\tIterateUsers(start, count int, fn UsersIterFn) bool\n\t}\n)\n\n// Permission defines the type for permissions.\ntype Permission uint16\n\n// String returns the string representation of a permission value.\nfunc (p Permission) String() string {\n\treturn strconv.FormatUint(uint64(p), 10)\n}\n"},{"name":"post.gno","body":"package boards\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\n// Post defines a generic type for posts.\n// A post can be either a thread or a reply.\ntype Post struct {\n\t// ID is the unique identifier of the post.\n\tID ID\n\n\t// ParentID is the ID of the parent post.\n\tParentID ID\n\n\t// ThreadID contains the post ID of the thread where current post is created.\n\t// If current post is a thread it contains post's ID.\n\t// It should be used when current post is a thread or reply.\n\tThreadID ID\n\n\t// OriginalBoardID contains the board ID of the original post when current post is a repost.\n\tOriginalBoardID ID\n\n\t// Board contains the board where post is created.\n\tBoard *Board\n\n\t// Title contains the post's title.\n\tTitle string\n\n\t// Body contains content of the post.\n\tBody string\n\n\t// Hidden indicates that the post is hidden.\n\tHidden bool\n\n\t// Readonly indicates that the post is readonly.\n\tReadonly bool\n\n\t// Replies stores post replies.\n\tReplies PostStorage\n\n\t// Reposts stores reposts of the current post.\n\t// It should be used when post is a thread.\n\tReposts RepostStorage\n\n\t// Flags stores users flags for the current post.\n\tFlags FlagStorage\n\n\t// Creator is the account address that created the post.\n\tCreator address\n\n\t// Meta allows storing post metadata.\n\tMeta any\n\n\t// CreatedAt is the post's creation time.\n\tCreatedAt time.Time\n\n\t// UpdatedAt is the post's update time.\n\tUpdatedAt time.Time\n}\n\n// Summary return a summary of the post's body.\n// It returns the body making sure that the length is limited to 80 characters.\nfunc (post Post) Summary() string {\n\treturn SummaryOf(post.Body, 80)\n}\n\n// SetID sets post ID value.\nfunc (post *Post) SetID(v ID) {\n\tpost.ID = v\n}\n\n// SetParentID sets post's parent ID value.\nfunc (post *Post) SetParentID(v ID) {\n\tpost.ParentID = v\n}\n\n// SetThreadID sets thread ID value.\nfunc (post *Post) SetThreadID(v ID) {\n\tpost.ThreadID = v\n}\n\n// SetOriginalBoardID sets the board ID of the original post when current post is a repost.\nfunc (post *Post) SetOriginalBoardID(v ID) {\n\tpost.OriginalBoardID = v\n}\n\n// SetBoard sets the board where post was created.\nfunc (post *Post) SetBoard(v *Board) {\n\tpost.Board = v\n}\n\n// SetTitle sets title value.\nfunc (post *Post) SetTitle(v string) {\n\tpost.Title = v\n}\n\n// SetBody sets post's content.\nfunc (post *Post) SetBody(v string) {\n\tpost.Body = v\n}\n\n// SetHidden sets hidden value.\nfunc (post *Post) SetHidden(v bool) {\n\tpost.Hidden = v\n}\n\n// SetReadonly sets readonly value.\nfunc (post *Post) SetReadonly(v bool) {\n\tpost.Readonly = v\n}\n\n// SetReplyStorage sets the storage where post replies are stored.\nfunc (post *Post) SetReplyStorage(v PostStorage) {\n\tpost.Replies = v\n}\n\n// SetRepostStorage sets the storage where thread reposts are stored.\nfunc (post *Post) SetRepostStorage(v RepostStorage) {\n\tpost.Reposts = v\n}\n\n// SetFlagStorage sets the storage where post flags are stored.\nfunc (post *Post) SetFlagStorage(v FlagStorage) {\n\tpost.Flags = v\n}\n\n// SetCreator sets the address of the account that created the post.\nfunc (post *Post) SetCreator(v address) {\n\tpost.Creator = v\n}\n\n// SetCreatedAt sets the time when post was created.\nfunc (post *Post) SetCreatedAt(v time.Time) {\n\tpost.CreatedAt = v\n}\n\n// SetUpdatedAt sets the time when a post value was updated.\nfunc (post *Post) SetUpdatedAt(v time.Time) {\n\tpost.UpdatedAt = v\n}\n\n// IsThread checks if a post is a thread.\n// When a post is not a thread it's considered a thread's reply/comment.\nfunc IsThread(p *Post) bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\treturn p.ThreadID == p.ID\n}\n\n// IsRepost checks if a thread is a repost.\nfunc IsRepost(thread *Post) bool {\n\tif thread == nil {\n\t\treturn false\n\t}\n\treturn thread.OriginalBoardID != 0\n}\n\n// SummaryOf returns a summary of a text.\nfunc SummaryOf(text string, length int) string {\n\ttext = strings.TrimSpace(text)\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\n\tlines := strings.SplitN(text, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n"},{"name":"post_storage.gno","body":"package boards\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\ntype (\n\t// PostIterFn defines a function type to iterate posts.\n\tPostIterFn func(*Post) bool\n\n\t// PostStorage defines an interface for posts storage.\n\tPostStorage interface {\n\t\t// Get retruns a post that matches an ID.\n\t\tGet(ID) (_ *Post, found bool)\n\n\t\t// Remove removes a post from the storage.\n\t\tRemove(ID) (_ *Post, removed bool)\n\n\t\t// Add adds a post in the storage.\n\t\tAdd(*Post) error\n\n\t\t// Size returns the number of posts in the storage.\n\t\tSize() int\n\n\t\t// Iterate iterates posts.\n\t\t// To reverse iterate posts use a negative count.\n\t\t// If the callback returns true, the iteration is stopped.\n\t\tIterate(start, count int, fn PostIterFn) bool\n\t}\n)\n\n// NewPostStorage creates a new storage for posts.\n// The new storage uses an AVL tree to store posts.\nfunc NewPostStorage() PostStorage {\n\treturn \u0026postStorage{bptree.NewBPTree32()}\n}\n\ntype postStorage struct {\n\tposts *bptree.BPTree // string(Post.ID) -\u003e *Post\n}\n\n// Get retruns a post that matches an ID.\nfunc (s postStorage) Get(id ID) (*Post, bool) {\n\tk := makePostKey(id)\n\tv := s.posts.Get(k)\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\treturn v.(*Post), true\n}\n\n// Remove removes a post from the storage.\nfunc (s *postStorage) Remove(id ID) (*Post, bool) {\n\tk := makePostKey(id)\n\tv, removed := s.posts.Remove(k)\n\tif !removed {\n\t\treturn nil, false\n\t}\n\treturn v.(*Post), true\n}\n\n// Add adds a post in the storage.\n// It updates existing posts when storage contains one with the same ID.\nfunc (s *postStorage) Add(p *Post) error {\n\tif p == nil {\n\t\treturn errors.New(\"saving nil posts is not allowed\")\n\t}\n\n\ts.posts.Set(makePostKey(p.ID), p)\n\treturn nil\n}\n\n// Size returns the number of posts in the storage.\nfunc (s postStorage) Size() int {\n\treturn s.posts.Size()\n}\n\n// Iterate iterates posts.\n// To reverse iterate posts use a negative count.\n// If the callback returns true, the iteration is stopped.\nfunc (s postStorage) Iterate(start, count int, fn PostIterFn) bool {\n\tif count \u003c 0 {\n\t\treturn s.posts.ReverseIterateByOffset(start, -count, func(_ string, v any) bool {\n\t\t\treturn fn(v.(*Post))\n\t\t})\n\t}\n\n\treturn s.posts.IterateByOffset(start, count, func(_ string, v any) bool {\n\t\treturn fn(v.(*Post))\n\t})\n}\n\nfunc makePostKey(postID ID) string {\n\treturn postID.PaddedString()\n}\n"},{"name":"reply.gno","body":"package boards\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewReply creates a new reply to a thread or another reply.\nfunc NewReply(parent *Post, creator address, body string) (*Post, error) {\n\tif parent == nil {\n\t\treturn nil, errors.New(\"reply requires a parent thread or reply\")\n\t}\n\n\tif parent.ThreadID == 0 {\n\t\treturn nil, errors.New(\"parent has no thread ID assigned\")\n\t}\n\n\tif parent.Board == nil {\n\t\treturn nil, errors.New(\"parent has no board assigned\")\n\t}\n\n\tif !creator.IsValid() {\n\t\treturn nil, ufmt.Errorf(\"invalid reply creator address: %s\", creator)\n\t}\n\n\tbody = strings.TrimSpace(body)\n\tif body == \"\" {\n\t\treturn nil, errors.New(\"reply body is required\")\n\t}\n\n\tid := parent.Board.ThreadsSequence.Next()\n\treturn \u0026Post{\n\t\tID:        id,\n\t\tParentID:  parent.ID,\n\t\tThreadID:  parent.ThreadID,\n\t\tBoard:     parent.Board,\n\t\tBody:      body,\n\t\tReplies:   NewPostStorage(),\n\t\tFlags:     NewFlagStorage(),\n\t\tCreator:   creator,\n\t\tCreatedAt: time.Now(),\n\t}, nil\n}\n\n// MustNewReply creates a new reply or panics on error.\nfunc MustNewReply(parent *Post, creator address, body string) *Post {\n\tp, err := NewReply(parent, creator, body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn p\n}\n"},{"name":"repost_storage.gno","body":"package boards\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\ntype (\n\t// RepostIterFn defines a function type to iterate reposts.\n\tRepostIterFn func(board, repost ID) bool\n\n\t// RepostStorage defines an interface for storing reposts.\n\tRepostStorage interface {\n\t\t// Get returns the repost ID for a board.\n\t\tGet(board ID) (repost ID, found bool)\n\n\t\t// Add adds a new repost to the storage.\n\t\tAdd(repost *Post) error\n\n\t\t// Remove removes repost for a board.\n\t\tRemove(board ID) (removed bool)\n\n\t\t// Size returns the number of reposts in the storage.\n\t\tSize() int\n\n\t\t// Iterate iterates reposts.\n\t\t// To reverse iterate reposts use a negative count.\n\t\t// If the callback returns true, the iteration is stopped.\n\t\tIterate(start, count int, fn RepostIterFn) bool\n\t}\n)\n\n// NewRepostStorage creates a new storage for reposts.\n// The new storage uses an AVL tree to store reposts.\nfunc NewRepostStorage() RepostStorage {\n\treturn \u0026repostStorage{bptree.NewBPTree32()}\n}\n\ntype repostStorage struct {\n\treposts *bptree.BPTree // string(Board.ID) -\u003e Post.ID\n}\n\n// Get returns the repost ID for a board.\nfunc (s repostStorage) Get(boardID ID) (ID, bool) {\n\tv := s.reposts.Get(boardID.Key())\n\tif v == nil {\n\t\treturn 0, false\n\t}\n\treturn v.(ID), true\n}\n\n// Add adds a new repost to the storage.\nfunc (s *repostStorage) Add(repost *Post) error {\n\tif repost == nil {\n\t\treturn errors.New(\"saving nil reposts is not allowed\")\n\t}\n\n\ts.reposts.Set(repost.Board.ID.Key(), repost.ID)\n\treturn nil\n}\n\n// Remove removes repost for a board.\nfunc (s *repostStorage) Remove(boardID ID) bool {\n\t_, removed := s.reposts.Remove(boardID.Key())\n\treturn removed\n}\n\n// Size returns the number of reposts in the storage.\nfunc (s repostStorage) Size() int {\n\treturn s.reposts.Size()\n}\n\n// Iterate iterates reposts.\n// To reverse iterate reposts use a negative count.\n// If the callback returns true, the iteration is stopped.\nfunc (s repostStorage) Iterate(start, count int, fn RepostIterFn) bool {\n\tif count \u003c 0 {\n\t\treturn s.reposts.ReverseIterateByOffset(start, -count, func(k string, v any) bool {\n\t\t\tid, err := seqid.FromString(k)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\n\t\t\treturn fn(ID(id), v.(ID))\n\t\t})\n\t}\n\n\treturn s.reposts.IterateByOffset(start, count, func(k string, v any) bool {\n\t\tid, err := seqid.FromString(k)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\treturn fn(ID(id), v.(ID))\n\t})\n}\n"},{"name":"storage.gno","body":"package boards\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\ntype (\n\t// BoardIterFn defines a function type to iterate boards.\n\tBoardIterFn func(*Board) bool\n\n\t// Storage defines an interface for boards storage.\n\tStorage interface {\n\t\t// Get retruns a boards that matches an ID.\n\t\tGet(ID) (_ *Board, found bool)\n\n\t\t// GetByName retruns a boards that matches a name.\n\t\tGetByName(name string) (_ *Board, found bool)\n\n\t\t// Remove removes a board from the storage.\n\t\tRemove(ID) (_ *Board, removed bool)\n\n\t\t// Add adds a board to the storage.\n\t\tAdd(*Board) error\n\n\t\t// Size returns the number of boards in the storage.\n\t\tSize() int\n\n\t\t// Iterate iterates boards.\n\t\t// To reverse iterate boards use a negative count.\n\t\t// If the callback returns true, the iteration is stopped.\n\t\tIterate(start, count int, fn BoardIterFn) bool\n\t}\n)\n\n// NewStorage creates a new boards storage.\nfunc NewStorage() Storage {\n\treturn \u0026storage{\n\t\tbyID:   bptree.NewBPTree32(),\n\t\tbyName: bptree.NewBPTree32(),\n\t}\n}\n\ntype storage struct {\n\tbyID   *bptree.BPTree // string(Board.ID) -\u003e *Board\n\tbyName *bptree.BPTree // Board.Name -\u003e Board.ID\n}\n\n// Get returns a board for a specific ID.\nfunc (s storage) Get(boardID ID) (*Board, bool) {\n\tkey := makeBoardKey(boardID)\n\tv := s.byID.Get(key)\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\treturn v.(*Board), true\n}\n\n// Get returns a board for a specific name.\nfunc (s storage) GetByName(name string) (*Board, bool) {\n\tkey := makeBoardNameKey(name)\n\tv := s.byName.Get(key)\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\treturn s.Get(v.(ID))\n}\n\n// Remove removes a board from the storage.\n// It returns false when board is not found.\nfunc (s *storage) Remove(boardID ID) (*Board, bool) {\n\tboard, found := s.Get(boardID)\n\tif !found {\n\t\treturn nil, false\n\t}\n\n\t// Remove indexes for current and previous board names\n\tnames := append([]string{board.Name}, board.Aliases...)\n\tfor _, name := range names {\n\t\tkey := makeBoardNameKey(name)\n\n\t\t// Make sure that name is indexed to the board being removed\n\t\tv := s.byName.Get(key)\n\t\tif v != nil \u0026\u0026 v.(ID) == boardID {\n\t\t\ts.byName.Remove(key)\n\t\t}\n\t}\n\n\tkey := makeBoardKey(board.ID)\n\t_, removed := s.byID.Remove(key)\n\treturn board, removed\n}\n\n// Add adds a board to the storage.\n// If board already exists it updates storage by reindexing the board by ID and name.\n// When board name changes it's indexed so it can be found with the new and previous names.\nfunc (s *storage) Add(board *Board) error {\n\tif board == nil {\n\t\treturn errors.New(\"adding nil boards to the storage is not allowed\")\n\t}\n\n\tkey := makeBoardKey(board.ID)\n\ts.byID.Set(key, board)\n\n\t// Index by name when the optional board name is not empty\n\tif key = makeBoardNameKey(board.Name); key != \"\" {\n\t\ts.byName.Set(key, board.ID)\n\t}\n\treturn nil\n}\n\n// Size returns the number of boards in the storage.\nfunc (s storage) Size() int {\n\treturn s.byID.Size()\n}\n\n// Iterate iterates boards.\n// To reverse iterate boards use a negative count.\n// If the callback returns true, the iteration is stopped.\nfunc (s storage) Iterate(start, count int, fn BoardIterFn) bool {\n\tif count \u003c 0 {\n\t\treturn s.byID.ReverseIterateByOffset(start, -count, func(_ string, v any) bool {\n\t\t\treturn fn(v.(*Board))\n\t\t})\n\t}\n\n\treturn s.byID.IterateByOffset(start, count, func(_ string, v any) bool {\n\t\treturn fn(v.(*Board))\n\t})\n}\n\nfunc makeBoardKey(boardID ID) string {\n\treturn boardID.Key()\n}\n\nfunc makeBoardNameKey(name string) string {\n\tname = strings.TrimSpace(name)\n\treturn strings.ToLower(name)\n}\n"},{"name":"thread.gno","body":"package boards\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewThread creates a new board thread.\nfunc NewThread(b *Board, creator address, title, body string) (*Post, error) {\n\tif b == nil {\n\t\treturn nil, errors.New(\"thread requires a parent board\")\n\t}\n\n\tif !creator.IsValid() {\n\t\treturn nil, ufmt.Errorf(\"invalid thread creator address: %s\", creator)\n\t}\n\n\ttitle = strings.TrimSpace(title)\n\tif title == \"\" {\n\t\treturn nil, errors.New(\"thread title is required\")\n\t}\n\n\tbody = strings.TrimSpace(body)\n\tif body == \"\" {\n\t\treturn nil, errors.New(\"thread body is required\")\n\t}\n\n\tid := b.ThreadsSequence.Next()\n\treturn \u0026Post{\n\t\tID:        id,\n\t\tThreadID:  id,\n\t\tBoard:     b,\n\t\tTitle:     title,\n\t\tBody:      body,\n\t\tReplies:   NewPostStorage(),\n\t\tReposts:   NewRepostStorage(),\n\t\tFlags:     NewFlagStorage(),\n\t\tCreator:   creator,\n\t\tCreatedAt: time.Now(),\n\t}, nil\n}\n\n// MustNewThread creates a new thread or panics on error.\nfunc MustNewThread(b *Board, creator address, title, body string) *Post {\n\tt, err := NewThread(b, creator, title, body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn t\n}\n\n// NewRepost creates a new thread that is a repost of a thread from another board.\nfunc NewRepost(thread *Post, dst *Board, creator address) (*Post, error) {\n\tif thread == nil {\n\t\treturn nil, errors.New(\"thread to repost is required\")\n\t}\n\n\tif thread.Board == nil {\n\t\treturn nil, errors.New(\"original thread has no board assigned\")\n\t}\n\n\tif dst == nil {\n\t\treturn nil, errors.New(\"thread repost requires a destination board\")\n\t}\n\n\tif IsRepost(thread) {\n\t\treturn nil, errors.New(\"reposting a thread that is a repost is not allowed\")\n\t}\n\n\tif !IsThread(thread) {\n\t\treturn nil, errors.New(\"post must be a thread to be reposted to another board\")\n\t}\n\n\tif !creator.IsValid() {\n\t\treturn nil, ufmt.Errorf(\"invalid thread repost creator address: %s\", creator)\n\t}\n\n\tid := dst.ThreadsSequence.Next()\n\treturn \u0026Post{\n\t\tID:              id,\n\t\tThreadID:        id,\n\t\tParentID:        thread.ID,\n\t\tOriginalBoardID: thread.Board.ID,\n\t\tBoard:           dst,\n\t\tReplies:         NewPostStorage(),\n\t\tReposts:         NewRepostStorage(),\n\t\tFlags:           NewFlagStorage(),\n\t\tCreator:         creator,\n\t\tCreatedAt:       time.Now(),\n\t}, nil\n}\n\n// MustNewRepost creates a new thread that is a repost of a thread from another board or panics on error.\nfunc MustNewRepost(thread *Post, dst *Board, creator address) *Post {\n\tr, err := NewRepost(thread, dst, creator)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn r\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"hub","path":"gno.land/p/gnoland/boards/exts/hub","files":[{"name":"README.md","body":"# hub\n\nSimplified, read-only view types over `gno.land/p/gnoland/boards` data:\n`Board`, `Thread`, `Comment`, `Flag` and `Member`.\n\nRealms use these to expose board contents through a query API without\nhanding callers access to their own persistent state.\n\n## Invariant\n\n**A safe type is a snapshot, never a live reference.**\n\nEach `NewSafe*` constructor reads the fields it needs off the `boards`\nvalue and copies them. It keeps no pointer to the source, so a value\nreturned to a caller cannot be used to reach — let alone mutate — the\nrealm data it was built from. Counts (`ThreadCount`, `FlagCount`, …) are\nresolved at construction time and do not track later changes.\n\nKeep it that way when adding fields:\n\n- Copy scalars. Never store the `*boards.Board` / `*boards.Post` the\n  constructor was handed, and never expose a `boards.PostStorage`,\n  `boards.FlagStorage` or `boards.Permissions` — those are handles onto\n  live realm state.\n- Deep-copy slices and maps, as `NewSafeBoard` does for `Aliases` and\n  `NewSafeMember` does for `Roles`. Copy on the way out too: a getter\n  that returns the stored slice lets a caller mutate the snapshot and\n  change what the same value reports on the next call, so `Aliases()`\n  and `Roles()` each return a fresh slice.\n- Convert `boards` types to plain ones where practical, the way `Member`\n  flattens `[]boards.Role` to `[]string`.\n\nAn earlier version of these types carried a `ref` field plus `Iterate*`\nmethods that walked realm storage through it. That is the thing this\npackage exists to not do.\n\n## Usage\n\n```go\nimport (\n    \"gno.land/p/gnoland/boards\"\n    hubexts \"gno.land/p/gnoland/boards/exts/hub\"\n)\n\nfunc GetBoard(id uint64) (hubexts.Board, bool) {\n    b, found := gBoards.Get(boards.ID(id))\n    if !found {\n        return hubexts.Board{}, false\n    }\n    return hubexts.NewSafeBoard(b), true\n}\n```\n\nThe constructors panic on a nil reference, and on a post whose kind does\nnot match (`NewSafeThread` on a comment, or `NewSafeComment` on a\nthread). Resolve and check existence before calling them.\n"},{"name":"board.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Board defines a safe type for boards.\ntype Board struct {\n\t// id is the unique identifier of the board.\n\tid uint64\n\n\t// name is the current name of the board.\n\tname string\n\n\t// aliases contains a list of alternative names for the board.\n\taliases []string\n\n\t// readonly indicates that the board is readonly.\n\treadonly bool\n\n\t// threadCount contains the number of threads within the board.\n\tthreadCount int\n\n\t// memberCount contains the number of members of the board.\n\tmemberCount int\n\n\t// creator is the account address that created the board.\n\tcreator address\n\n\t// createdAt is the board's creation time as Unix time.\n\tcreatedAt int64\n\n\t// updatedAt is the board's update time as Unix time.\n\tupdatedAt int64\n}\n\n// ID returns the unique identifier of the board.\nfunc (b Board) ID() uint64 { return b.id }\n\n// Name returns the current name of the board.\nfunc (b Board) Name() string { return b.name }\n\n// Aliases returns the list of alternative names for the board.\nfunc (b Board) Aliases() []string { return append([]string(nil), b.aliases...) }\n\n// Readonly indicates that the board is readonly.\nfunc (b Board) Readonly() bool { return b.readonly }\n\n// ThreadCount returns the number of threads within the board.\nfunc (b Board) ThreadCount() int { return b.threadCount }\n\n// MemberCount returns the number of members of the board.\nfunc (b Board) MemberCount() int { return b.memberCount }\n\n// Creator returns the account address that created the board.\nfunc (b Board) Creator() address { return b.creator }\n\n// CreatedAt returns the board's creation time as Unix time.\nfunc (b Board) CreatedAt() int64 { return b.createdAt }\n\n// UpdatedAt returns the board's update time as Unix time.\nfunc (b Board) UpdatedAt() int64 { return b.updatedAt }\n\n// NewSafeBoard creates a safe board.\nfunc NewSafeBoard(ref *boards.Board) Board {\n\tif ref == nil {\n\t\tpanic(\"board reference is nil\")\n\t}\n\n\tvar usersCount int\n\tif ref.Permissions != nil {\n\t\tusersCount = ref.Permissions.UsersCount()\n\t}\n\n\tvar threadCount int\n\tif ref.Threads != nil {\n\t\tthreadCount = ref.Threads.Size()\n\t}\n\n\treturn Board{\n\t\tid:          uint64(ref.ID),\n\t\tname:        ref.Name,\n\t\taliases:     append([]string(nil), ref.Aliases...),\n\t\treadonly:    ref.Readonly,\n\t\tthreadCount: threadCount,\n\t\tmemberCount: usersCount,\n\t\tcreator:     ref.Creator,\n\t\tcreatedAt:   timeToUnix(ref.CreatedAt),\n\t\tupdatedAt:   timeToUnix(ref.UpdatedAt),\n\t}\n}\n"},{"name":"comment.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Comment defines a type for threads comment/replies.\ntype Comment struct {\n\t// id is the unique identifier of the comment.\n\tid uint64\n\n\t// boardID is the board ID where comment is created.\n\tboardID uint64\n\n\t// threadID is the ID of the thread where comment is created.\n\tthreadID uint64\n\n\t// parentID is the ID of the parent comment or reply.\n\tparentID uint64\n\n\t// body contains the comment's content.\n\tbody string\n\n\t// hidden indicates that comment is hidden.\n\thidden bool\n\n\t// replyCount contains the number of comments replies.\n\t// Count only includes top level replies, sub-replies are not included.\n\treplyCount int\n\n\t// flagCount contains the number of flags that comment has.\n\tflagCount int\n\n\t// creator is the account address that created the comment or reply.\n\tcreator address\n\n\t// createdAt is thread's creation time as Unix time.\n\tcreatedAt int64\n\n\t// updatedAt is thread's update time as Unix time.\n\tupdatedAt int64\n}\n\n// ID returns the unique identifier of the comment.\nfunc (c Comment) ID() uint64 { return c.id }\n\n// BoardID returns the board ID where the comment is created.\nfunc (c Comment) BoardID() uint64 { return c.boardID }\n\n// ThreadID returns the ID of the thread where the comment is created.\nfunc (c Comment) ThreadID() uint64 { return c.threadID }\n\n// ParentID returns the ID of the parent comment or reply.\nfunc (c Comment) ParentID() uint64 { return c.parentID }\n\n// Body returns the comment's content.\nfunc (c Comment) Body() string { return c.body }\n\n// Hidden indicates that the comment is hidden.\nfunc (c Comment) Hidden() bool { return c.hidden }\n\n// ReplyCount returns the number of comment replies.\n// Count only includes top level replies, sub-replies are not included.\nfunc (c Comment) ReplyCount() int { return c.replyCount }\n\n// FlagCount returns the number of flags that the comment has.\nfunc (c Comment) FlagCount() int { return c.flagCount }\n\n// Creator returns the account address that created the comment or reply.\nfunc (c Comment) Creator() address { return c.creator }\n\n// CreatedAt returns the comment's creation time as Unix time.\nfunc (c Comment) CreatedAt() int64 { return c.createdAt }\n\n// UpdatedAt returns the comment's update time as Unix time.\nfunc (c Comment) UpdatedAt() int64 { return c.updatedAt }\n\n// NewSafeComment creates a safe comment.\nfunc NewSafeComment(ref *boards.Post) Comment {\n\tif ref == nil {\n\t\tpanic(\"post reference is nil\")\n\t}\n\tif boards.IsThread(ref) {\n\t\tpanic(\"post is not a comment or reply\")\n\t}\n\n\tvar replyCount int\n\tif ref.Replies != nil {\n\t\treplyCount = ref.Replies.Size()\n\t}\n\n\tvar flagCount int\n\tif ref.Flags != nil {\n\t\tflagCount = ref.Flags.Size()\n\t}\n\n\treturn Comment{\n\t\tid:         uint64(ref.ID),\n\t\tboardID:    uint64(ref.Board.ID),\n\t\tthreadID:   uint64(ref.ThreadID),\n\t\tparentID:   uint64(ref.ParentID),\n\t\tbody:       ref.Body,\n\t\thidden:     ref.Hidden,\n\t\treplyCount: replyCount,\n\t\tflagCount:  flagCount,\n\t\tcreator:    ref.Creator,\n\t\tcreatedAt:  timeToUnix(ref.CreatedAt),\n\t\tupdatedAt:  timeToUnix(ref.UpdatedAt),\n\t}\n}\n"},{"name":"flag.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Flag defines a type for thread and comment flags.\ntype Flag struct {\n\t// user is the user that flagged.\n\tuser address\n\n\t// reason is the reason for flagging.\n\treason string\n}\n\n// User returns the user that flagged.\nfunc (f Flag) User() address { return f.user }\n\n// Reason returns the reason for flagging.\nfunc (f Flag) Reason() string { return f.reason }\n\n// NewSafeFlag creates a safe flag.\nfunc NewSafeFlag(ref boards.Flag) Flag {\n\treturn Flag{\n\t\tuser:   ref.User,\n\t\treason: ref.Reason,\n\t}\n}\n"},{"name":"format.gno","body":"package hub\n\nimport \"time\"\n\n// timeToUnix converts time to Unix epoch.\nfunc timeToUnix(t time.Time) int64 {\n\tif t.IsZero() {\n\t\treturn 0\n\t}\n\treturn t.Unix()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/gnoland/boards/exts/hub\"\ngno = \"0.9\"\n"},{"name":"member.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Member defines a safe type for board members.\ntype Member struct {\n\t// address is the account address of the member.\n\taddress address\n\n\t// roles contains the names of the roles assigned to the member.\n\troles []string\n}\n\n// Address returns the account address of the member.\nfunc (m Member) Address() address { return m.address }\n\n// Roles returns the names of the roles assigned to the member.\nfunc (m Member) Roles() []string { return append([]string(nil), m.roles...) }\n\n// NewSafeMember creates a safe board member.\nfunc NewSafeMember(ref boards.User) Member {\n\troles := make([]string, len(ref.Roles))\n\tfor i, r := range ref.Roles {\n\t\troles[i] = string(r)\n\t}\n\n\treturn Member{\n\t\taddress: ref.Address,\n\t\troles:   roles,\n\t}\n}\n"},{"name":"thread.gno","body":"package hub\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Thread defines a type for board threads.\ntype Thread struct {\n\t// id is the unique identifier of the thread.\n\tid uint64\n\n\t// originalBoardID contains the board ID of the original thread when current is a repost.\n\toriginalBoardID uint64\n\n\t// originalThreadID contains the ID of the original thread when current is a repost.\n\toriginalThreadID uint64\n\n\t// boardID is the board ID where thread is created.\n\tboardID uint64\n\n\t// title contains thread's title.\n\ttitle string\n\n\t// body contains content of the thread.\n\tbody string\n\n\t// hidden indicates that thread is hidden.\n\thidden bool\n\n\t// readonly indicates that thread is readonly.\n\treadonly bool\n\n\t// commentCount contains the number of thread comments.\n\t// Count only includes top level comment, replies are not included.\n\tcommentCount int\n\n\t// repostCount contains the number of times thread has been reposted.\n\trepostCount int\n\n\t// flagCount contains the number of flags that thread has.\n\tflagCount int\n\n\t// creator is the account address that created the thread.\n\tcreator address\n\n\t// createdAt is thread's creation time as Unix time.\n\tcreatedAt int64\n\n\t// updatedAt is thread's update time as unix time.\n\tupdatedAt int64\n}\n\n// ID returns the unique identifier of the thread.\nfunc (t Thread) ID() uint64 { return t.id }\n\n// OriginalBoardID returns the board ID of the original thread when current is a repost.\nfunc (t Thread) OriginalBoardID() uint64 { return t.originalBoardID }\n\n// OriginalThreadID returns the ID of the original thread when current is a repost.\nfunc (t Thread) OriginalThreadID() uint64 { return t.originalThreadID }\n\n// BoardID returns the board ID where the thread is created.\nfunc (t Thread) BoardID() uint64 { return t.boardID }\n\n// Title returns the thread's title.\nfunc (t Thread) Title() string { return t.title }\n\n// Body returns the content of the thread.\nfunc (t Thread) Body() string { return t.body }\n\n// Hidden indicates that the thread is hidden.\nfunc (t Thread) Hidden() bool { return t.hidden }\n\n// Readonly indicates that the thread is readonly.\nfunc (t Thread) Readonly() bool { return t.readonly }\n\n// CommentCount returns the number of thread comments.\n// Count only includes top level comment, replies are not included.\nfunc (t Thread) CommentCount() int { return t.commentCount }\n\n// RepostCount returns the number of times the thread has been reposted.\nfunc (t Thread) RepostCount() int { return t.repostCount }\n\n// FlagCount returns the number of flags that the thread has.\nfunc (t Thread) FlagCount() int { return t.flagCount }\n\n// Creator returns the account address that created the thread.\nfunc (t Thread) Creator() address { return t.creator }\n\n// CreatedAt returns the thread's creation time as Unix time.\nfunc (t Thread) CreatedAt() int64 { return t.createdAt }\n\n// UpdatedAt returns the thread's update time as Unix time.\nfunc (t Thread) UpdatedAt() int64 { return t.updatedAt }\n\n// NewSafeThread creates a safe thread.\nfunc NewSafeThread(ref *boards.Post) Thread {\n\tif ref == nil {\n\t\tpanic(\"post reference is nil\")\n\t}\n\tif !boards.IsThread(ref) {\n\t\tpanic(\"post is not a thread\")\n\t}\n\n\tvar commentCount int\n\tif ref.Replies != nil {\n\t\tcommentCount = ref.Replies.Size()\n\t}\n\n\tvar repostCount int\n\tif ref.Reposts != nil {\n\t\trepostCount = ref.Reposts.Size()\n\t}\n\n\tvar flagCount int\n\tif ref.Flags != nil {\n\t\tflagCount = ref.Flags.Size()\n\t}\n\n\treturn Thread{\n\t\tid:               uint64(ref.ID),\n\t\toriginalBoardID:  uint64(ref.OriginalBoardID),\n\t\toriginalThreadID: uint64(ref.ParentID),\n\t\tboardID:          uint64(ref.Board.ID),\n\t\ttitle:            ref.Title,\n\t\tbody:             ref.Body,\n\t\thidden:           ref.Hidden,\n\t\treadonly:         ref.Readonly,\n\t\tcommentCount:     commentCount,\n\t\trepostCount:      repostCount,\n\t\tflagCount:        flagCount,\n\t\tcreator:          ref.Creator,\n\t\tcreatedAt:        timeToUnix(ref.CreatedAt),\n\t\tupdatedAt:        timeToUnix(ref.UpdatedAt),\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"fifo","path":"gno.land/p/moul/fifo","files":[{"name":"fifo.gno","body":"// Package fifo implements a fixed-size FIFO (First-In-First-Out) list data structure\n// using a singly-linked list. The implementation prioritizes storage efficiency by minimizing\n// storage operations - each add/remove operation only updates 1-2 pointers, regardless of\n// list size.\n//\n// Key features:\n// - Fixed-size with automatic removal of oldest entries when full\n// - Support for both prepend (add at start) and append (add at end) operations\n// - Constant storage usage through automatic pruning\n// - O(1) append operations and latest element access\n// - Iterator support for sequential access\n// - Dynamic size adjustment via SetMaxSize\n//\n// This implementation is optimized for frequent updates, as insertions and deletions only\n// require updating 1-2 pointers. However, random access operations are O(n) as they require\n// traversing the list. For use cases where writes are rare, a slice-based\n// implementation might be more suitable.\n//\n// The linked list structure is equally efficient for storing both small values (like pointers)\n// and larger data structures, as each node maintains a single next-pointer regardless of the\n// stored value's size.\n//\n// Example usage:\n//\n//\tlist := fifo.New(3)        // Create a new list with max size 3\n//\tlist.Append(\"a\")           // List: [a]\n//\tlist.Append(\"b\")           // List: [a b]\n//\tlist.Append(\"c\")           // List: [a b c]\n//\tlist.Append(\"d\")           // List: [b c d] (oldest element \"a\" was removed)\n//\tlatest := list.Latest()    // Returns \"d\"\n//\tall := list.Entries()      // Returns [\"b\", \"c\", \"d\"]\npackage fifo\n\n// node represents a single element in the linked list\ntype node struct {\n\tvalue any\n\tnext  *node\n}\n\n// List represents a fixed-size FIFO list\ntype List struct {\n\thead    *node\n\ttail    *node\n\tsize    int\n\tmaxSize int\n}\n\n// New creates a new FIFO list with the specified maximum size\nfunc New(maxSize int) *List {\n\treturn \u0026List{\n\t\tmaxSize: maxSize,\n\t}\n}\n\n// Prepend adds a new entry at the start of the list. If the list exceeds maxSize,\n// the last entry is automatically removed.\nfunc (l *List) Prepend(entry any) {\n\tif l.maxSize == 0 {\n\t\treturn\n\t}\n\n\tnewNode := \u0026node{value: entry}\n\n\tif l.head == nil {\n\t\tl.head = newNode\n\t\tl.tail = newNode\n\t\tl.size = 1\n\t\treturn\n\t}\n\n\tnewNode.next = l.head\n\tl.head = newNode\n\n\tif l.size \u003c l.maxSize {\n\t\tl.size++\n\t\treturn\n\t}\n\n\t// Remove last element by traversing to second-to-last\n\tif l.size == 1 {\n\t\t// Special case: if size is 1, just update both pointers\n\t\tl.head = newNode\n\t\tl.tail = newNode\n\t\tnewNode.next = nil\n\t\treturn\n\n\t}\n\n\t// Find second-to-last node\n\tcurrent := l.head\n\tfor current.next != l.tail {\n\t\tcurrent = current.next\n\t}\n\tcurrent.next = nil\n\tl.tail = current\n\n}\n\n// Append adds a new entry at the end of the list. If the list exceeds maxSize,\n// the first entry is automatically removed.\nfunc (l *List) Append(entry any) {\n\tif l.maxSize == 0 {\n\t\treturn\n\t}\n\n\tnewNode := \u0026node{value: entry}\n\n\tif l.head == nil {\n\t\tl.head = newNode\n\t\tl.tail = newNode\n\t\tl.size = 1\n\t\treturn\n\t}\n\n\tl.tail.next = newNode\n\tl.tail = newNode\n\n\tif l.size \u003c l.maxSize {\n\t\tl.size++\n\t} else {\n\t\tl.head = l.head.next\n\t}\n}\n\n// Get returns the entry at the specified index.\n// Index 0 is the oldest entry, Size()-1 is the newest.\nfunc (l *List) Get(index int) any {\n\tif index \u003c 0 || index \u003e= l.size {\n\t\treturn nil\n\t}\n\n\tcurrent := l.head\n\tfor i := 0; i \u003c index; i++ {\n\t\tcurrent = current.next\n\t}\n\treturn current.value\n}\n\n// Size returns the current number of entries in the list\nfunc (l *List) Size() int {\n\treturn l.size\n}\n\n// MaxSize returns the maximum size configured for this list\nfunc (l *List) MaxSize() int {\n\treturn l.maxSize\n}\n\n// Entries returns all current entries as a slice\nfunc (l *List) Entries() []any {\n\tentries := make([]any, l.size)\n\tcurrent := l.head\n\tfor i := 0; i \u003c l.size; i++ {\n\t\tentries[i] = current.value\n\t\tcurrent = current.next\n\t}\n\treturn entries\n}\n\n// Iterator returns a function that can be used to iterate over the entries\n// from oldest to newest. Returns nil when there are no more entries.\nfunc (l *List) Iterator() func() any {\n\tcurrent := l.head\n\treturn func() any {\n\t\tif current == nil {\n\t\t\treturn nil\n\t\t}\n\t\tvalue := current.value\n\t\tcurrent = current.next\n\t\treturn value\n\t}\n}\n\n// Latest returns the most recent entry.\n// Returns nil if the list is empty.\nfunc (l *List) Latest() any {\n\tif l.tail == nil {\n\t\treturn nil\n\t}\n\treturn l.tail.value\n}\n\n// SetMaxSize updates the maximum size of the list.\n// If the new maxSize is smaller than the current size,\n// the oldest entries are removed to fit the new size.\nfunc (l *List) SetMaxSize(maxSize int) {\n\tif maxSize \u003c 0 {\n\t\tmaxSize = 0\n\t}\n\n\t// If new maxSize is smaller than current size,\n\t// remove oldest entries until we fit\n\tif maxSize \u003c l.size {\n\t\t// Special case: if new maxSize is 0, clear the list\n\t\tif maxSize == 0 {\n\t\t\tl.head = nil\n\t\t\tl.tail = nil\n\t\t\tl.size = 0\n\t\t} else {\n\t\t\t// Keep the newest entries by moving head forward\n\t\t\tdiff := l.size - maxSize\n\t\t\tfor i := 0; i \u003c diff; i++ {\n\t\t\t\tl.head = l.head.next\n\t\t\t}\n\t\t\tl.size = maxSize\n\t\t}\n\t}\n\n\tl.maxSize = maxSize\n}\n\n// Delete removes the element at the specified index.\n// Returns true if an element was removed, false if the index was invalid.\nfunc (l *List) Delete(index int) bool {\n\tif index \u003c 0 || index \u003e= l.size {\n\t\treturn false\n\t}\n\n\t// Special case: deleting the only element\n\tif l.size == 1 {\n\t\tl.head = nil\n\t\tl.tail = nil\n\t\tl.size = 0\n\t\treturn true\n\t}\n\n\t// Special case: deleting first element\n\tif index == 0 {\n\t\tl.head = l.head.next\n\t\tl.size--\n\t\treturn true\n\t}\n\n\t// Find the node before the one to delete\n\tcurrent := l.head\n\tfor i := 0; i \u003c index-1; i++ {\n\t\tcurrent = current.next\n\t}\n\n\t// Special case: deleting last element\n\tif index == l.size-1 {\n\t\tl.tail = current\n\t\tcurrent.next = nil\n\t} else {\n\t\tcurrent.next = current.next.next\n\t}\n\n\tl.size--\n\treturn true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/fifo\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"realmpath","path":"gno.land/p/moul/realmpath","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/realmpath\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"realmpath.gno","body":"// Package realmpath is a lightweight Render.path parsing and link generation\n// library with an idiomatic API, closely resembling that of net/url.\n//\n// This package provides utilities for parsing request paths and query\n// parameters, allowing you to extract path segments and manipulate query\n// values.\n//\n// Example usage:\n//\n//\timport \"gno.land/p/moul/realmpath\"\n//\n//\tfunc Render(path string) string {\n//\t    // Parsing a sample path with query parameters\n//\t    path = \"hello/world?foo=bar\u0026baz=foobar\"\n//\t    req := realmpath.Parse(path)\n//\n//\t    // Accessing parsed path and query parameters\n//\t    println(req.Path)             // Output: hello/world\n//\t    println(req.PathPart(0))      // Output: hello\n//\t    println(req.PathPart(1))      // Output: world\n//\t    println(req.Query.Get(\"foo\")) // Output: bar\n//\t    println(req.Query.Get(\"baz\")) // Output: foobar\n//\n//\t    // Rebuilding the URL\n//\t    println(req.String())         // Output: /r/current/realm:hello/world?baz=foobar\u0026foo=bar\n//\t}\npackage realmpath\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"net/url\"\n\t\"strings\"\n)\n\nvar chainDomain = runtime.ChainDomain()\n\n// Request represents a parsed request.\ntype Request struct {\n\tPath  string     // The path of the request\n\tQuery url.Values // The parsed query parameters\n\tRealm string     // The realm associated with the request\n}\n\n// Parse takes a raw path string and returns a Request object.\n// It splits the path into its components and parses any query parameters.\nfunc Parse(rawPath string) *Request {\n\t// Split the raw path into path and query components\n\tpath, query := splitPathAndQuery(rawPath)\n\n\t// Parse the query string into url.Values\n\tqueryValues, _ := url.ParseQuery(query)\n\n\treturn \u0026Request{\n\t\tPath:  path,        // Set the path\n\t\tQuery: queryValues, // Set the parsed query values\n\t}\n}\n\n// PathParts returns the segments of the path as a slice of strings.\n// It trims leading and trailing slashes and splits the path by slashes.\nfunc (r *Request) PathParts() []string {\n\treturn strings.Split(strings.Trim(r.Path, \"/\"), \"/\")\n}\n\n// PathPart returns the specified part of the path.\n// If the index is out of bounds, it returns an empty string.\nfunc (r *Request) PathPart(index int) string {\n\tparts := r.PathParts() // Get the path segments\n\tif index \u003c 0 || index \u003e= len(parts) {\n\t\treturn \"\" // Return empty if index is out of bounds\n\t}\n\treturn parts[index] // Return the specified path part\n}\n\n// String rebuilds the URL from the path and query values.\n// If the Realm is not set, it automatically retrieves the current realm path.\n//\n// SECURITY (Class-2-shaped, intentionally accepted): unsafe.CurrentRealm()\n// inside a non-crossing /p/ method is .Title()-vulnerable — it walks past\n// non-crossing frames to the most-recent crossing ancestor, not the\n// immediate caller. The lazily-captured r.Realm is therefore NOT a reliable\n// identity claim under a contrived call chain.\n//\n// This is acceptable here because r.Realm is consumed only as a URL string\n// for rendering (the line below builds reconstructedPath for display); no\n// caller in /examples uses it for authorization. If you add a new consumer\n// that gates writes/auth on r.Realm, replace the lazy fill with an explicit\n// `req.Realm = cur.PkgPath()` set by the caller under rlm.IsCurrent(), or\n// switch to a sibling method that takes a realm parameter — see\n// docs/resources/gno-security.md for the threat-class taxonomy.\nfunc (r *Request) String() string {\n\t// Automatically set the Realm if it is not already defined\n\tif r.Realm == \"\" {\n\t\tr.Realm = unsafe.CurrentRealm().PkgPath() // Get the current realm path\n\t}\n\n\t// Rebuild the path using the realm and path parts\n\trelativePkgPath := strings.TrimPrefix(r.Realm, chainDomain) // Trim the chain domain prefix\n\treconstructedPath := relativePkgPath + \":\" + strings.Join(r.PathParts(), \"/\")\n\n\t// Rebuild the query string\n\tqueryString := r.Query.Encode() // Encode the query parameters\n\tif queryString != \"\" {\n\t\treturn reconstructedPath + \"?\" + queryString // Return the full URL with query\n\t}\n\treturn reconstructedPath // Return the path without query parameters\n}\n\nfunc splitPathAndQuery(rawPath string) (string, string) {\n\tif idx := strings.Index(rawPath, \"?\"); idx != -1 {\n\t\treturn rawPath[:idx], rawPath[idx+1:] // Split at the first '?' found\n\t}\n\treturn rawPath, \"\" // No query string present\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"profile","path":"gno.land/r/demo/profile","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/profile\"\ngno = \"0.9\"\n"},{"name":"profile.gno","body":"package profile\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tfields = avl.NewTree()\n\trouter = mux.NewRouter()\n)\n\n// Standard fields\nconst (\n\tDisplayName        = \"DisplayName\"\n\tHomepage           = \"Homepage\"\n\tBio                = \"Bio\"\n\tAge                = \"Age\"\n\tLocation           = \"Location\"\n\tAvatar             = \"Avatar\"\n\tGravatarEmail      = \"GravatarEmail\"\n\tAvailableForHiring = \"AvailableForHiring\"\n\tInvalidField       = \"InvalidField\"\n)\n\n// Events\nconst (\n\tProfileFieldCreated = \"ProfileFieldCreated\"\n\tProfileFieldUpdated = \"ProfileFieldUpdated\"\n)\n\n// Field types used when emitting event\nconst FieldType = \"FieldType\"\n\nconst (\n\tBoolField   = \"BoolField\"\n\tStringField = \"StringField\"\n\tIntField    = \"IntField\"\n)\n\nfunc init() {\n\trouter.HandleFunc(\"\", homeHandler)\n\trouter.HandleFunc(\"u/{addr}\", profileHandler)\n\trouter.HandleFunc(\"f/{addr}/{field}\", fieldHandler)\n}\n\n// List of supported string fields\nvar stringFields = map[string]bool{\n\tDisplayName:   true,\n\tHomepage:      true,\n\tBio:           true,\n\tLocation:      true,\n\tAvatar:        true,\n\tGravatarEmail: true,\n}\n\n// List of support int fields\nvar intFields = map[string]bool{\n\tAge: true,\n}\n\n// List of support bool fields\nvar boolFields = map[string]bool{\n\tAvailableForHiring: true,\n}\n\n// Setters\n\nfunc SetStringField(cur realm, field, value string) bool {\n\taddr := cur.Previous().Address()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tchain.Emit(event, FieldType, StringField, field, value)\n\n\treturn updated\n}\n\nfunc SetIntField(cur realm, field string, value int) bool {\n\taddr := cur.Previous().Address()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tchain.Emit(event, FieldType, IntField, field, string(value))\n\n\treturn updated\n}\n\nfunc SetBoolField(cur realm, field string, value bool) bool {\n\taddr := cur.Previous().Address()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tchain.Emit(event, FieldType, BoolField, field, ufmt.Sprintf(\"%t\", value))\n\n\treturn updated\n}\n\n// Getters\n\nfunc GetStringField(addr address, field, def string) string {\n\tkey := addr.String() + \":\" + field\n\tif value := fields.Get(key); value != nil {\n\t\treturn value.(string)\n\t}\n\n\treturn def\n}\n\nfunc GetBoolField(addr address, field string, def bool) bool {\n\tkey := addr.String() + \":\" + field\n\tif value := fields.Get(key); value != nil {\n\t\treturn value.(bool)\n\t}\n\n\treturn def\n}\n\nfunc GetIntField(addr address, field string, def int) int {\n\tkey := addr.String() + \":\" + field\n\tif value := fields.Get(key); value != nil {\n\t\treturn value.(int)\n\t}\n\n\treturn def\n}\n"},{"name":"render.gno","body":"package profile\n\nimport (\n\t\"bytes\"\n\t\"net/url\"\n\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tBaseURL           = \"/r/demo/profile\"\n\tSetStringFieldURL = BaseURL + \"$help\u0026func=SetStringField\u0026field=%s\"\n\tSetIntFieldURL    = BaseURL + \"$help\u0026func=SetIntField\u0026field=%s\"\n\tSetBoolFieldURL   = BaseURL + \"$help\u0026func=SetBoolField\u0026field=%s\"\n\tViewAllFieldsURL  = BaseURL + \":u/%s\"\n\tViewFieldURL      = BaseURL + \":f/%s/%s\"\n)\n\nfunc homeHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"## Setters\\n\")\n\tfor field := range stringFields {\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range intFields {\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s Field](%s)\\n\", field, link))\n\t}\n\n\tb.WriteString(\"\\n---\\n\\n\")\n\n\tres.Write(b.String())\n}\n\nfunc profileHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Profile %s\\n\", addr))\n\n\taddress_XXX := address(addr)\n\n\tfor field := range stringFields {\n\t\tvalue := GetStringField(address_XXX, field, \"n/a\")\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range intFields {\n\t\tvalue := GetIntField(address_XXX, field, 0)\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %d [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tvalue := GetBoolField(address_XXX, field, false)\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %t [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tres.Write(b.String())\n}\n\nfunc fieldHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\tfield := req.GetVar(\"field\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Field %s for %s\\n\", field, addr))\n\n\taddress_XXX := address(addr)\n\tvalue := \"n/a\"\n\tvar editLink string\n\n\tif _, ok := stringFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%s\", GetStringField(address_XXX, field, \"n/a\"))\n\t\teditLink = ufmt.Sprintf(SetStringFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, url.QueryEscape(value))\n\t} else if _, ok := intFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%d\", GetIntField(address_XXX, field, 0))\n\t\teditLink = ufmt.Sprintf(SetIntFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t} else if _, ok := boolFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%t\", GetBoolField(address_XXX, field, false))\n\t\teditLink = ufmt.Sprintf(SetBoolFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t}\n\n\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, editLink))\n\n\tres.Write(b.String())\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"namereg","path":"gno.land/r/sys/namereg/v1","files":[{"name":"admin.gno","body":"package namereg\n\nimport (\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nvar paused = false // XXX: replace with p/moul/authz\n\n//----------------------------------------\n// Privileged mutators.\n\nfunc setPaused(cur realm, newPausedValue bool) {\n\tpaused = newPausedValue\n}\n\nfunc updateUsername(cur realm, userData *susers.UserData, newName string) error {\n\t// UpdateName must be called from this realm.\n\treturn userData.UpdateName(0, cur, newName)\n}\n\nfunc deleteUserdata(cur realm, userData *susers.UserData) error {\n\t// Delete must be called from this realm.\n\treturn userData.Delete(0, cur)\n}\n\nfunc setRegisterPrice(cur realm, newPrice int64) {\n\tregisterPrice = newPrice\n}\n\n//----------------------------------------\n// Public API\n\n// NewSetPausedExecutor allows GovDAO to pause or unpause this realm\nfunc NewSetPausedExecutor(cur realm, newPausedValue bool) dao.ProposalRequest {\n\tcb := func(cur realm) error {\n\t\tsetPaused(cur, newPausedValue)\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\tif newPausedValue {\n\t\treturn dao.NewProposalRequest(\"User Registry V1: Pause\", \"\", e)\n\t}\n\n\treturn dao.NewProposalRequest(\"User Registry V1: Unpause\", \"\", e)\n}\n\n// ProposeNewName allows GovDAO to propose a new name for an existing user.\n// The associated address and all previous names of a user that changes a\n// name are preserved, and all resolve to the new name.\n//\n// Governance renames bypass the Open Nym Tier `nym-...\\d{3}` format intentionally.\n// The DAO is the trust root for this realm; if voters approve a rename to\n// `vitalik` (e.g. for trademark dispute resolution or system reservations),\n// the validation here imposes no further opinion. The new name is still\n// subject to the base shape enforced by `r/sys/users.validateName`\n// (`^[a-z][a-z0-9]*([_-][a-z0-9]+)*$`, max 64 chars), which runs inside\n// the updateUsername callback below.\nfunc ProposeNewName(cur realm, addr address, newName string) dao.ProposalRequest {\n\tuserData := susers.ResolveAddress(addr)\n\tif userData == nil {\n\t\tpanic(susers.ErrUserNotExistOrDeleted)\n\t}\n\n\tcb := func(cur realm) error {\n\t\terr := updateUsername(cur, userData, newName)\n\t\treturn err\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\treturn dao.NewProposalRequest(\n\t\tufmt.Sprintf(\"User Registry V1: Rename user `%s` to `%s`\", userData.Name(), newName),\n\t\t\"\",\n\t\te,\n\t)\n}\n\n// ProposeDeleteUser allows GovDAO to propose deletion of a user\n// This will make the associated address and names unresolvable.\n// WARN: After deletion, the same address WILL NOT be able to register a new name.\nfunc ProposeDeleteUser(cur realm, addr address, reason string) dao.ProposalRequest {\n\tuserData := susers.ResolveAddress(addr)\n\tif userData == nil {\n\t\tpanic(susers.ErrUserNotExistOrDeleted)\n\t}\n\n\tcb := func(cur realm) error {\n\t\treturn deleteUserdata(cur, userData)\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\treturn dao.NewProposalRequest(\n\t\tufmt.Sprintf(\"User Registry V1: Delete user `%s`\", userData.Name()),\n\t\treason,\n\t\te,\n\t)\n}\n\n// ProposeNewRegisterPrice allows GovDAO to update the price of registration.\n// Rejects prices below MinRegisterPrice (currently 0) at proposal-creation\n// time. (audit finding #14: original code only rejected negative values,\n// which would have been arithmetically nonsensical.)\nfunc ProposeNewRegisterPrice(cur realm, newPrice int64) dao.ProposalRequest {\n\tif newPrice \u003c MinRegisterPrice {\n\t\tpanic(ufmt.Sprintf(\"price below floor: %d ugnot \u003c %d ugnot (MinRegisterPrice)\",\n\t\t\tnewPrice, MinRegisterPrice))\n\t}\n\n\tcb := func(cur realm) error {\n\t\tsetRegisterPrice(cur, newPrice)\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\treturn dao.NewProposalRequest(\n\t\tufmt.Sprintf(\"User Registry V1: Update registration price to `%d`\", newPrice),\n\t\t\"\",\n\t\te,\n\t)\n}\n"},{"name":"api.gno","body":"package namereg\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// Open Nym Tier username format. Anchored.\n//   - literal `nym-` prefix (4 chars)\n//   - 5-13 lowercase ASCII letters (the alpha stem)\n//   - exactly 3 trailing decimal digits\n//\n// Total length 12-20 chars. Distinct by length from `g1...` addresses\n// which are always 40 chars.\nconst reNymFormat = `^nym-[a-z]{5,13}\\d{3}$`\n\nvar reNym = regexp.MustCompile(reNymFormat)\n\n// Reserved alpha-stem prefixes. Names whose stem starts with one of\n// these are rejected at format-validation time with ErrReservedPrefix\n// (clearer than ErrCanonicalCollision after the fact).\n//\n// `gi` is intentionally NOT listed: in most rendering targets `i` is\n// visually distinct enough from `1`/`l` that legitimate `gi*` names\n// (giggles, gimbal, gift, etc.) should remain registerable. Phishing\n// protection for the visual class is still enforced by canonical-\n// collision detection in r/sys/users — once any `gi*` or `gl*` name\n// is registered, all variants under the {l,i,1}→i canonicalization\n// collide.\n//\n// `gl` and `g1` remain listed because they're more visually\n// confusable with the bech32 address prefix `g1`. `g1` itself is\n// unreachable through the alpha-only stem regex; defense-in-depth\n// for any future regex relaxation.\nvar reservedPrefixes = []string{\"gl\", \"g1\", \"gno\", \"atom\", \"atone\", \"photon\", \"cosmos\"}\n\n// Exported error sentinels returned by ValidateNymFormat. Use\n// errors.Is or direct equality; do not string-match.\n//\n// ErrReservedPrefix's message is built from reservedPrefixes at package\n// init time so the surfaced list never drifts from the actual policy.\n//\n// Canonical-collision detection moved to r/sys/users in Option B.\n// Consumers that previously caught namereg.ErrCanonicalCollision\n// should switch to susers.ErrCanonicalCollision.\nvar (\n\tErrInvalidFormat  = errors.New(\"namereg: name must match nym-[a-z]{5,13}\\\\d{3}\")\n\tErrReservedPrefix = errors.New(\"namereg: stem starts with a reserved prefix (\" + strings.Join(reservedPrefixes, \"/\") + \")\")\n\tErrBlacklisted    = errors.New(\"namereg: stem matches a reserved role name\")\n)\n\n// IsReserved reports whether the given alpha stem matches a reserved\n// role name (with implicit `s`-suffix expansion). The check is\n// canonicalized — so `vital1k`-style l-substituted variants of a\n// reserved name are also caught. O(1) backed by `reservedSet` built\n// in init().\nfunc IsReserved(stem string) bool {\n\t_, found := reservedSet[Canonicalize(stem)]\n\treturn found\n}\n\n// ValidateNymFormat checks the regex, prefix-exclusion, and reserved-\n// name rules in that order. Returns one of the exported sentinel\n// errors per failure mode, or nil on success.\n//\n// Does NOT run the canonical-collision check — that lives in r/sys/users\n// (susers.IsCanonicalTaken or, atomically with the write, inside\n// susers.RegisterUser).\nfunc ValidateNymFormat(username string) error {\n\tif !reNym.MatchString(username) {\n\t\treturn ErrInvalidFormat\n\t}\n\n\t// Stem is everything between `nym-` (4 chars) and the trailing\n\t// 3 digits. Regex guarantees 5..13 alpha chars in this slice.\n\tstem := username[4 : len(username)-3]\n\n\tfor _, p := range reservedPrefixes {\n\t\tif strings.HasPrefix(stem, p) {\n\t\t\treturn ErrReservedPrefix\n\t\t}\n\t}\n\n\tif IsReserved(stem) {\n\t\treturn ErrBlacklisted\n\t}\n\n\treturn nil\n}\n\n// IsPaused exposes the realm's pause flag for cross-controller\n// coordination.\nfunc IsPaused() bool {\n\treturn paused\n}\n"},{"name":"blacklist.gno","body":"package namereg\n\n// reservedNames lists role/system identifiers that must never be allocated\n// as a registered name. The intent is to prevent Open Nym Tier\n// auto-registrations like \"nym-admin000\" from impersonating system roles.\n//\n// Sources merged here:\n//   - Common role names already covered by Handshake's valid.json (the 90k\n//     curated trademark/gTLD-application list) — admin, help, support, etc.\n//   - Common role names NOT covered by Handshake — administrator, root,\n//     sysadmin, owner, staff, api, etc.\n//   - RFC 2606 / RFC 6761 reserved labels — example, invalid, localhost,\n//     local, test.\n//\n// Plural rule: every entry below is ALSO reserved with the literal \"s\"\n// suffix appended. So \"doc\" reserves both \"doc\" and \"docs\"; \"setting\"\n// reserves both \"setting\" and \"settings\"; \"new\" covers \"news\", and so on.\n// Entries are stored in the singular here and the validator appends \"s\"\n// at check time. This halves list maintenance and avoids the temptation\n// to add `name+\"s\"` after every singular entry.\n//\n// Note on length: the Open Nym Tier regex restricts the [a-z]{5,13}\n// middle to 5–13 chars, so entries shorter than 5 (mod, api, bot, god,\n// gno, ...) and longer than 13 (administrator, jesuschrist,\n// newtendermint, ...) cannot appear in Register() even without this list.\n// They are kept anyway because:\n//\n//\t(a) cross-reference value — a single canonical list is easier to audit\n//\t    than two scope-specific lists with overlapping intent; and\n//\t(b) future controllers (e.g. a hypothetical DAO-allocated path) can\n//\t    opt in to the same blacklist by querying IsReserved, providing\n//\t    defense-in-depth across the registration ecosystem.\n//\n// IMPORTANT: this list is NOT consulted by `ProposeNewName` — governance\n// renames are gated by GovDAO vote alone, not by this blacklist. Voters\n// reviewing a rename proposal are responsible for catching collisions\n// with reserved names.\n//\n// Sortedness: entries must be sorted lexicographically (Go's \u003c on strings,\n// which is byte-wise ASCII). TestReservedNamesSorted enforces this.\nvar reservedNames = []string{\n\t\"about\",\n\t\"abuse\",\n\t\"account\",\n\t\"admin\",\n\t\"administrator\",\n\t\"aib\",\n\t\"aibinc\",\n\t\"allinbits\",\n\t\"allinbitsinc\",\n\t\"anonymous\",\n\t\"api\",\n\t\"atom\",\n\t\"atomone\",\n\t\"atomonehub\",\n\t\"atone\",\n\t\"atonehub\",\n\t\"bitcoin\",\n\t\"blockchain\",\n\t\"blog\",\n\t\"bot\",\n\t\"chain\",\n\t\"coin\",\n\t\"community\",\n\t\"contact\",\n\t\"cosmos\",\n\t\"cosmoshub\",\n\t\"crypto\",\n\t\"daemon\",\n\t\"dashboard\",\n\t\"default\",\n\t\"demo\",\n\t\"doc\",\n\t\"domain\",\n\t\"email\",\n\t\"ether\",\n\t\"ethereum\",\n\t\"everyone\",\n\t\"example\",\n\t\"gno\",\n\t\"gnoland\",\n\t\"gnolang\",\n\t\"gnome\",\n\t\"gnot\",\n\t\"gnoworld\",\n\t\"god\",\n\t\"gov\",\n\t\"govdao\",\n\t\"governance\",\n\t\"guest\",\n\t\"help\",\n\t\"home\",\n\t\"host\",\n\t\"info\",\n\t\"invalid\",\n\t\"jaekwon\",\n\t\"jesus\",\n\t\"jesuschrist\",\n\t\"local\",\n\t\"localhost\",\n\t\"login\",\n\t\"mail\",\n\t\"mod\",\n\t\"moderator\",\n\t\"new\",\n\t\"newtendermint\",\n\t\"newtendermintllc\",\n\t\"nt\",\n\t\"ntllc\",\n\t\"null\",\n\t\"owner\",\n\t\"photon\",\n\t\"profile\",\n\t\"register\",\n\t\"registry\",\n\t\"resolution\",\n\t\"resolve\",\n\t\"resolver\",\n\t\"root\",\n\t\"security\",\n\t\"service\",\n\t\"setting\",\n\t\"shop\",\n\t\"signup\",\n\t\"staff\",\n\t\"store\",\n\t\"support\",\n\t\"sys\",\n\t\"sysadmin\",\n\t\"system\",\n\t\"team\",\n\t\"tendermint\",\n\t\"test\",\n\t\"user\",\n}\n"},{"name":"canonical.gno","body":"package namereg\n\nimport (\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// reservedSet is the runtime O(1) lookup for the role-name blacklist.\n// Built in init() from reservedNames in blacklist.gno: each source\n// entry contributes BOTH `Canonicalize(n)` and `Canonicalize(n+\"s\")`\n// as keys, implementing the \"implicit `s` suffix\" rule documented on\n// reservedNames.\n//\n// Why canonicalize the blacklist itself: validation canonicalizes the\n// candidate stem before checking, so the comparison set must also be\n// in canonical form. Otherwise a candidate like \"vital1k\" would\n// canonicalize to \"vitaiik\" but the blacklist would contain only\n// \"vitalik\" — the comparison would miss the match. Keeping both sides\n// in canonical form makes the lookup exact.\n//\n// The blacklist remains namereg/v1-local because it is policy specific\n// to the Open Nym Tier. Other controllers may have entirely different\n// reserved-name policies, or none at all.\nvar reservedSet map[string]struct{}\n\nfunc init() {\n\treservedSet = make(map[string]struct{}, len(reservedNames)*2)\n\tfor _, n := range reservedNames {\n\t\treservedSet[Canonicalize(n)] = struct{}{}\n\t\treservedSet[Canonicalize(n+\"s\")] = struct{}{}\n\t}\n}\n\n// Canonicalize is a delegating shim to r/sys/users.Canonicalize.\n//\n// HISTORY: namereg/v1 used to host its own per-stem canonical store and\n// its own Canonicalize (l→i only). Option B unified the canonical lookup\n// into r/sys/users keyed by full canonical name with broader\n// substitutions ({l,i,1}→i, {0,o}→o, {-,.,_} stripped). This shim\n// preserves the call-site name for local consumers (blacklist init,\n// IsReserved) and any external consumer that imported the function\n// from namereg/v1 before Option B.\n//\n// New code should call susers.Canonicalize directly.\nfunc Canonicalize(s string) string {\n\treturn susers.Canonicalize(s)\n}\n"},{"name":"errors.gno","body":"package namereg\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tErrNonUserCall     = errors.New(\"r/gnoland/users: non-user call\")\n\tErrPaused          = errors.New(\"r/gnoland/users: paused\")\n\tErrInvalidUsername = errors.New(\"r/gnoland/users: invalid username\")\n\n\t// ErrInvalidPayment is the sentinel for \"OriginSend amount didn't\n\t// match registerPrice.\" It deliberately omits the price from its\n\t// message — the price is read at panic time via errInvalidPayment()\n\t// below, so users see the CURRENT price even after governance has\n\t// changed it. Tests that match against this error use it as a\n\t// substring check via uassert.AbortsWithMessage. (audit finding #13)\n\tErrInvalidPayment = errors.New(\"r/gnoland/users: invalid payment amount\")\n)\n\n// errInvalidPayment returns the panic value for an OriginSend mismatch.\n// Constructed lazily so the formatted price reflects the current value\n// of registerPrice rather than the value frozen at package init time.\n// Replaces the old `ErrInvalidPayment = ufmt.Errorf(...)` package var\n// (audit finding #13) which captured registerPrice once and silently\n// drifted out of date after every ProposeNewRegisterPrice execution.\nfunc errInvalidPayment() error {\n\treturn ufmt.Errorf(\"%s: must send exactly %d ugnot\",\n\t\tErrInvalidPayment.Error(), registerPrice)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/namereg/v1\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"init.gno","body":"package namereg\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\n\tsusers \"gno.land/r/sys/users\"\n)\n\nfunc init(cur realm) {\n\tif runtime.ChainHeight() == 0 {\n\t\tsusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/namereg/v1\"))\n\t}\n}\n"},{"name":"preregister.gno","body":"package namereg\n\nimport (\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Pre-registered names bypass the Open Nym Tier `nym-...\\d{3}` format\n// intentionally. They are bootstrap names allocated at genesis and are\n// not subject to the auto-registration regex. They DO appear in\n// reservedNames as a defense-in-depth (so a future governance-bypass\n// path can't accidentally reallocate them either), but at this layer\n// they are written directly via susers.RegisterUserIgnoreCanonical.\n//\n// Uses the bypass path so curated confusables in the seed (e.g.\n// `gnoland` and `gnolang` both canonicalize distinctly today, but a\n// future addition that collides must not abort chain bring-up).\n// Matches the genesis posture in r/sys/users/init.gno.\n\n// pre-registered users\nvar preRegisteredUsers = []struct {\n\tName    string\n\tAddress address\n}{\n\t// system names.\n\t// the goal is to make them either team/DAO-owned or ownerless.\n\t{\"archive\", \"g1xlnyjrnf03ju82v0f98ruhpgnquk28knmjfe5k\"}, // -\u003e @archive\n\t{\"demo\", \"g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n\"},    // -\u003e @demo\n\t{\"gno\", \"g19602kd9tfxrfd60sgreadt9zvdyyuudcyxsz8a\"},     // -\u003e @gno\n\t{\"gnoland\", \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"}, // -\u003e @gnoland\n\t{\"gnolang\", \"g1yjlnm3z2630gg5mryjd79907e0zx658wxs9hnd\"}, // -\u003e @gnolang\n\t{\"gov\", \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"},     // -\u003e @gov\n\t{\"nt\", \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"},      // -\u003e @nt\n\t{\"sys\", \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"},     // -\u003e @sys\n\t{\"x\", \"g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz\"},       // -\u003e @x\n\n\t// test1 user\n\t{\"test1\", \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"}, // -\u003e @test1\n}\n\nfunc init(cur realm) {\n\t// add pre-registered users via the bypass path (genesis posture).\n\t// Errors are intentionally discarded; the seed is curated and any\n\t// duplicate-address / already-deleted entries (re-init across test\n\t// realm reuse) just no-op.\n\tfor _, res := range preRegisteredUsers {\n\t\tsusers.RegisterUserIgnoreCanonical(cross(cur), res.Name, res.Address)\n\t}\n}\n"},{"name":"render.gno","body":"package namereg\n\nimport (\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/demo/profile\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nfunc Render(path string) string {\n\treq := realmpath.Parse(path)\n\n\tif req.Path == \"\" {\n\t\treturn renderHomePage()\n\t}\n\n\t// Otherwise, render the user page\n\treturn renderUserPage(req.Path)\n}\n\nfunc renderHomePage() string {\n\tvar out string\n\n\tout += \"# Gno.land User Registry\\n\"\n\n\tif paused {\n\t\tout += md.HorizontalRule()\n\t\tout += md.H2(\"This realm is paused.\")\n\t\tout += md.Paragraph(\"Check out [`gno.land/r/sys/users`](/r/sys/users) for the current user registry.\")\n\t\tout += md.HorizontalRule()\n\t}\n\n\tout += renderIntroParagraph()\n\n\tout += md.H2(\"Latest registrations\")\n\tout += RenderLatestUsersWidget(-1)\n\n\treturn out\n}\n\nfunc renderIntroParagraph() string {\n\tout := md.Paragraph(\"Welcome to the Gno.land User Registry (v1). Please register a username.\")\n\tout += md.Paragraph(`Registering a username grants the registering address the right to deploy packages and realms\nunder that username’s namespace. For example, if an address registers the username ` + md.InlineCode(\"nym-alice123\") + `, it\nwill gain permission to deploy packages and realms to package paths with the pattern ` + md.InlineCode(\"gno.land/{p,r}/nym-alice123/*\") + `.`)\n\n\tout += md.Paragraph(\"In V1, usernames must match `nym-\u003cstem\u003e\u003cdigits\u003e`, where:\")\n\titems := []string{\n\t\t\"`\u003cstem\u003e` is 5 to 13 lowercase ASCII letters\",\n\t\t\"`\u003cdigits\u003e` is exactly 3 decimal digits\",\n\t\t\"The stem must NOT start with `gno`, `gl`, `atom`, `atone`, `photon`, or `cosmos` (reserved prefixes)\",\n\t\t\"The stem must NOT match a reserved role name (admin, root, support, ...)\",\n\t\t\"Confusable variants (e.g. `vitaiik` vs `vitalik` via `l↔i`) are blocked via canonical-form collision detection\",\n\t\t\"Total username length: 12–20 chars (distinct from `g1...` addresses, which are always 40 chars)\",\n\t}\n\tout += md.BulletList(items)\n\n\tout += \"\\n\\n\"\n\tout += md.Paragraph(\"Vanity names outside this format may be allocated by GovDAO governance through `ProposeNewName`.\")\n\n\tif !paused {\n\t\tamount := ufmt.Sprintf(\"%dugnot\", registerPrice)\n\t\tlink := txlink.NewLink(\"Register\")\n\t\tif registerPrice \u003e 0 {\n\t\t\tlink = link.SetSend(amount)\n\t\t}\n\n\t\tout += md.H3(ufmt.Sprintf(\" [[Click here to register]](%s)\", link.URL()))\n\t\t// XXX: Display registration price adjusting for dynamic GNOT price when it becomes possible.\n\t\tout += ufmt.Sprintf(\"Registration price: %f GNOT (%s)\\n\\n\", float64(registerPrice)/1_000_000, amount)\n\t}\n\n\tout += md.HorizontalRule()\n\tout += \"\\n\\n\"\n\n\treturn out\n}\n\n// resolveUser resolves the user based on the path, determining if it's a name or address\nfunc resolveUser(path string) (*susers.UserData, bool, bool) {\n\tif address(path).IsValid() {\n\t\treturn susers.ResolveAddress(address(path)), false, false\n\t}\n\n\tdata, isLatest := susers.ResolveName(path)\n\treturn data, isLatest, true\n}\n\n// renderUserPage generates the user page based on user data and path\nfunc renderUserPage(path string) string {\n\tvar out string\n\n\t// Render single user page\n\tdata, isLatest, isName := resolveUser(path)\n\tif data == nil {\n\t\tout += md.H1(\"User not found.\")\n\t\tout += \"This user does not exist or has been deleted.\\n\"\n\t\treturn out\n\t}\n\n\tout += md.H1(\"User - \" + md.InlineCode(data.Name()))\n\n\tif isName \u0026\u0026 !isLatest {\n\t\tout += md.Paragraph(ufmt.Sprintf(\n\t\t\t\"Note: You searched for `%s`, which is a previous name of [`%s`](/u/%s).\",\n\t\t\tpath, data.Name(), data.Name()))\n\t} else {\n\t\tout += ufmt.Sprintf(\"Address: %s\\n\\n\", data.Addr().String())\n\n\t\tout += md.H2(\"Bio\")\n\t\tout += profile.GetStringField(data.Addr(), \"Bio\", \"No bio defined.\")\n\t\tout += \"\\n\\n\"\n\t\tout += ufmt.Sprintf(\"[Update bio](%s)\", txlink.Realm(\"gno.land/r/demo/profile\").Call(\"SetStringField\", \"field\", \"Bio\"))\n\t\tout += \"\\n\\n\"\n\t}\n\n\treturn out\n}\n\n// RenderLatestUsersWidget renders the latest num registered users.\n// For num = -1, the maximum number (100) will be displayed.\nfunc RenderLatestUsersWidget(num int) string {\n\tsize := latestUsers.Size()\n\tif size == 0 {\n\t\treturn \"No registered users.\"\n\t}\n\n\tif num \u003e size || num \u003c 0 {\n\t\tnum = size\n\t}\n\n\tentries := latestUsers.Entries()\n\tvar out string\n\n\tfor i := size - 1; i \u003e= size-num; i-- {\n\t\tuser := entries[i].(string)\n\t\tout += md.BulletItem(md.UserLink(user))\n\t}\n\n\treturn out\n}\n"},{"name":"users.gno","body":"package namereg\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/moul/fifo\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// MinRegisterPrice is the lowest price (in ugnot) that\n// ProposeNewRegisterPrice will accept. Set to 0 — registration is free\n// by default; governance can raise the price via ProposeNewRegisterPrice\n// without a floor.\nconst MinRegisterPrice = int64(0)\n\nvar (\n\tregisterPrice = int64(0)      // free by default; governance can raise via ProposeNewRegisterPrice\n\tlatestUsers   = fifo.New(100) // Save the latest 100 users for rendering purposes\n)\n\n// Register registers a new username for the caller.\n//\n// Valid usernames match `nym-[a-z]{5,13}\\d{3}`:\n//   - literal `nym-` prefix (4 chars)\n//   - 5-13 lowercase letters (the alpha stem)\n//   - exactly 3 trailing decimal digits\n//\n// Total length 12-20 chars. The alpha stem additionally must NOT start\n// with `gno`/`gi`/`gl` and must not match a reserved role name (with\n// implicit `s`-suffix expansion). See ValidateNymFormat for the\n// format/blacklist check.\n//\n// Canonical-collision detection is enforced atomically by\n// susers.RegisterUser via the unified canonical store in r/sys/users\n// (decision: per Option B, every controller participates in the same\n// canonical-form lookup keyed by full canonical name).\n//\n// Only direct EOA (maketx call) invocations are supported.\nfunc Register(cur realm, username string) {\n\t// Anti-squatting payment check, two paired guards:\n\t//\n\t//   (a) PreviousRealm must be a pure EOA (IsUserCall: pkgPath == \"\").\n\t//       This excludes intermediate code realms AND user-run ephemeral\n\t//       realms (\"maketx run\" scripts). Both can attach -send to the\n\t//       tx but spend the coins on something other than forwarding to\n\t//       this realm, leaving OriginSend() describing a phantom payment.\n\t//       IsUserCall is the only PreviousRealm shape where the tx-send\n\t//       envelope is guaranteed to have landed at this realm's address.\n\t//\n\t//   (b) OriginSend amount must exactly equal registerPrice. Verifies\n\t//       the tx actually attached the expected amount.\n\t//\n\t// Both checks MUST run together. Removing (a) alone makes (b) meaningless\n\t// because OriginSend() describes tx intent, not realm receipt.\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(ErrNonUserCall)\n\t}\n\n\tif paused {\n\t\tpanic(ErrPaused)\n\t}\n\n\tif unsafe.OriginSend().AmountOf(\"ugnot\") != registerPrice {\n\t\tpanic(errInvalidPayment())\n\t}\n\n\t// Format + prefix + reserved-name check. ValidateNymFormat returns\n\t// one of ErrInvalidFormat, ErrReservedPrefix, ErrBlacklisted.\n\tif err := ValidateNymFormat(username); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Delegate the canonical-collision check + nameStore write atomically\n\t// to r/sys/users. Returns susers.ErrCanonicalCollision if the\n\t// canonical form clashes with an existing registration in any\n\t// controller.\n\tregistrant := cur.Previous().Address()\n\tif err := susers.RegisterUser(cross(cur), username, registrant); err != nil {\n\t\tpanic(err)\n\t}\n\n\tlatestUsers.Append(username)\n\tchain.Emit(\"Registration\", \"address\", registrant.String(), \"name\", username)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"validators","path":"gno.land/p/sys/validators","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/sys/validators\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"types.gno","body":"package validators\n\nimport (\n\t\"errors\"\n)\n\n// ValsetProtocol defines the validator set protocol (PoA / PoS / PoC / ?)\ntype ValsetProtocol interface {\n\t// AddValidator adds a new validator to the validator set.\n\t// If the validator is already present, the method should error out\n\t//\n\t// TODO: This API is not ideal -- the address should be derived from\n\t// the public key, and not be passed in as such, but currently Gno\n\t// does not support crypto address derivation\n\tAddValidator(address_XXX address, pubKey string, power uint64) (Validator, error)\n\n\t// RemoveValidator removes the given validator from the set.\n\t// If the validator is not present in the set, the method should error out\n\tRemoveValidator(address_XXX address) (Validator, error)\n\n\t// IsValidator returns a flag indicating if the given\n\t// bech32 address is part of the validator set\n\tIsValidator(address_XXX address) bool\n\n\t// GetValidator returns the validator using the given address\n\tGetValidator(address_XXX address) (Validator, error)\n\n\t// GetValidators returns the currently active validator set\n\tGetValidators() []Validator\n}\n\n// Validator represents a single chain validator\ntype Validator struct {\n\tAddress     address // bech32 address\n\tPubKey      string  // bech32 representation of the public key\n\tVotingPower uint64\n}\n\nconst (\n\tValidatorAddedEvent   = \"ValidatorAdded\"   // emitted when a validator was added to the set\n\tValidatorRemovedEvent = \"ValidatorRemoved\" // emitted when a validator was removed from the set\n)\n\nvar (\n\t// ErrValidatorExists is returned when the validator is already in the set\n\tErrValidatorExists = errors.New(\"validator already exists\")\n\n\t// ErrValidatorMissing is returned when the validator is not in the set\n\tErrValidatorMissing = errors.New(\"validator doesn't exist\")\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"poa","path":"gno.land/p/nt/poa/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `poa` - Proof of Authority validator set\n\nStateful Proof of Authority validator set with simple add/remove constraints. This is a low-level building block intended to be embedded by chain-level governance code (e.g. a GovDAO bridge to `gno.land/p/sys/validators`), not a typical realm utility.\n\nConstraints:\n- **Add**: validator must not be in the set already and voting power must be `\u003e 0`.\n- **Remove**: validator must be in the set.\n\n## Usage\n\n```go\nimport (\n    \"gno.land/p/nt/poa/v0\"\n    \"gno.land/p/sys/validators\"\n)\n\n// Start with a pre-seeded validator set.\nset := poa.NewPoA(poa.WithInitialSet([]validators.Validator{\n    {Address: \"g1...\", PubKey: \"gpub1...\", VotingPower: 10},\n}))\n\n// Add a validator.\nv, err := set.AddValidator(\"g1xyz...\", \"gpub1xyz...\", 5)\nif err != nil {\n    panic(err)\n}\n\n// Inspect membership.\nif set.IsValidator(\"g1xyz...\") {\n    // ...\n}\n\n// List the full current set.\nall := set.GetValidators()\n\n// Remove a validator.\nremoved, err := set.RemoveValidator(v.Address)\n```\n\n## API\n\n```go\ntype PoA struct { /* ... */ }\n\n// Construct an empty set; options seed initial validators.\nfunc NewPoA(opts ...Option) *PoA\n\n// WithInitialSet seeds the validator set at construction time.\nfunc WithInitialSet(vs []validators.Validator) Option\n\nfunc (p *PoA) AddValidator(addr address, pubKey string, power uint64) (validators.Validator, error)\nfunc (p *PoA) RemoveValidator(addr address) (validators.Validator, error)\nfunc (p *PoA) IsValidator(addr address) bool\nfunc (p *PoA) GetValidator(addr address) (validators.Validator, error)\nfunc (p *PoA) GetValidators() []validators.Validator\n```\n\nValidators are stored and returned as `validators.Validator` from `gno.land/p/sys/validators`.\n\n## Errors\n\n- `ErrInvalidVotingPower` — `AddValidator` called with `power == 0`.\n- `validators.ErrValidatorExists` — adding an address already in the set.\n- `validators.ErrValidatorMissing` — removing or fetching an address that is not in the set.\n\n## Notes\n\n- Public keys are stored as-is — there is no on-chain verification yet (`TODO` in source).\n- The package is intentionally narrow: it only manages the in-memory set. Consensus-layer wiring (proposing/applying changes to the actual validator set) is the caller's responsibility.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package poa implements a Proof of Authority validator set management system.\npackage poa\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/poa/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"},{"name":"option.gno","body":"package poa\n\nimport \"gno.land/p/sys/validators\"\n\ntype Option func(*PoA)\n\n// WithInitialSet sets the initial PoA validator set\nfunc WithInitialSet(validators []validators.Validator) Option {\n\treturn func(p *PoA) {\n\t\tfor _, validator := range validators {\n\t\t\tp.validators.Set(validator.Address.String(), validator)\n\t\t}\n\t}\n}\n"},{"name":"poa.gno","body":"package poa\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar ErrInvalidVotingPower = errors.New(\"invalid voting power\")\n\n// PoA specifies the Proof of Authority validator set, with simple add / remove constraints.\n//\n// To add:\n// - proposed validator must not be part of the set already\n// - proposed validator voting power must be \u003e 0\n//\n// To remove:\n// - proposed validator must be part of the set already\ntype PoA struct {\n\tvalidators *bptree.BPTree // address -\u003e validators.Validator\n}\n\n// NewPoA creates a new empty Proof of Authority validator set\nfunc NewPoA(opts ...Option) *PoA {\n\t// Create the empty set\n\tp := \u0026PoA{\n\t\tvalidators: bptree.NewBPTree32(),\n\t}\n\n\t// Apply the options\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\n\treturn p\n}\n\nfunc (p *PoA) AddValidator(address_XXX address, pubKey string, power uint64) (validators.Validator, error) {\n\t// Validate that the operation is a valid call.\n\t// Check if the validator is already in the set\n\tif p.IsValidator(address_XXX) {\n\t\treturn validators.Validator{}, validators.ErrValidatorExists\n\t}\n\n\t// Make sure the voting power \u003e 0\n\tif power == 0 {\n\t\treturn validators.Validator{}, ErrInvalidVotingPower\n\t}\n\n\tv := validators.Validator{\n\t\tAddress:     address_XXX,\n\t\tPubKey:      pubKey, // TODO: in the future, verify the public key\n\t\tVotingPower: power,\n\t}\n\n\t// Add the validator to the set\n\tp.validators.Set(address_XXX.String(), v)\n\n\treturn v, nil\n}\n\nfunc (p *PoA) RemoveValidator(address_XXX address) (validators.Validator, error) {\n\t// Validate that the operation is a valid call\n\t// Fetch the validator\n\tvalidator, err := p.GetValidator(address_XXX)\n\tif err != nil {\n\t\treturn validators.Validator{}, err\n\t}\n\n\t// Remove the validator from the set\n\tp.validators.Remove(address_XXX.String())\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) IsValidator(address_XXX address) bool {\n\treturn p.validators.Has(address_XXX.String())\n}\n\nfunc (p *PoA) GetValidator(address_XXX address) (validators.Validator, error) {\n\tvalidatorRaw := p.validators.Get(address_XXX.String())\n\tif validatorRaw == nil {\n\t\treturn validators.Validator{}, validators.ErrValidatorMissing\n\t}\n\n\tvalidator := validatorRaw.(validators.Validator)\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) GetValidators() []validators.Validator {\n\tvals := make([]validators.Validator, 0, p.validators.Size())\n\n\tp.validators.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tvalidator := value.(validators.Validator)\n\t\tvals = append(vals, validator)\n\n\t\treturn false\n\t})\n\n\treturn vals\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"validators","path":"gno.land/r/sys/validators/v2","files":[{"name":"doc.gno","body":"// Package validators implements the on-chain validator set management through Proof of Contribution.\n// The Realm exposes only a public executor for govdao proposals, that can suggest validator set changes.\npackage validators\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/validators/v2\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"gnosdk.gno","body":"package validators\n\nimport (\n\t\"math\"\n\n\t\"gno.land/p/sys/validators\"\n)\n\n// GetChanges returns the validator changes stored on the realm,\n// for blocks in the [from, to] range (inclusive on both ends).\n// If to \u003e= math.MaxInt64, it is clamped to math.MaxInt64-1 to avoid overflow.\n// Panics if from \u003e to (after clamping).\n// This function is intended to be called by gno.land through the GnoSDK.\nfunc GetChanges(from, to int64) []validators.Validator {\n\tif to \u003e math.MaxInt64-1 {\n\t\tto = math.MaxInt64 - 1\n\t}\n\tif to \u003c from {\n\t\tpanic(\"invalid range: from must be \u003c= to\")\n\t}\n\n\tvalsetChanges := make([]validators.Validator, 0)\n\n\t// Gather the changes in the [from, to] block range.\n\t// AVL Iterate uses an exclusive end, so we pass to+1.\n\tchanges.Iterate(getBlockID(from), getBlockID(to+1), func(_ string, value any) bool {\n\t\tchs := value.([]change)\n\n\t\tfor _, ch := range chs {\n\t\t\tvalsetChanges = append(valsetChanges, ch.validator)\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn valsetChanges\n}\n"},{"name":"init.gno","body":"package validators\n\nimport (\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/poa/v0\"\n)\n\nfunc init() {\n\t// The default valset protocol is PoA\n\tvp = poa.NewPoA()\n\n\t// No changes to apply initially\n\tchanges = bptree.NewBPTree32()\n}\n"},{"name":"poc.gno","body":"package validators\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// NewPropRequest creates a new proposal request that wraps a changes closure\n// proposal. This wrapper is required to ensure the GovDAO Realm actually\n// executed the callback.\nfunc NewPropRequest(cur realm, changesFn func() []validators.Validator, title, description string) dao.ProposalRequest {\n\tif changesFn == nil {\n\t\tpanic(\"no set changes proposed\")\n\t}\n\n\ttitle = strings.TrimSpace(title)\n\tif title == \"\" {\n\t\tpanic(\"proposal title is empty\")\n\t}\n\n\t// Get the list of validators now to make sure the list\n\t// doesn't change during the lifetime of the proposal\n\tchanges := changesFn()\n\n\t// Limit the number of validators to keep the description within a limit\n\t// that makes sense because there is not pagination of validators\n\tif len(changes) \u003e 40 {\n\t\tpanic(\"max number of allowed validators per proposal is 40\")\n\t} else if len(changes) == 0 {\n\t\tpanic(\"proposal requires at least one validator\")\n\t}\n\n\t// List the validator addresses and the action to be taken for each one\n\tvar desc strings.Builder\n\tdesc.WriteString(description)\n\tif len(description) \u003e 0 {\n\t\tdesc.WriteString(\"\\n\\n\")\n\t}\n\n\tdesc.WriteString(\"## Validator Updates\\n\")\n\tfor _, change := range changes {\n\t\tif change.VotingPower == 0 {\n\t\t\tdesc.WriteString(ufmt.Sprintf(\"- %s: remove\\n\", change.Address))\n\t\t} else {\n\t\t\tdesc.WriteString(ufmt.Sprintf(\"- %s: add\\n\", change.Address))\n\t\t}\n\t}\n\n\tcallback := func(cur realm) error {\n\t\tfor _, change := range changes {\n\t\t\tif change.VotingPower == 0 {\n\t\t\t\t// This change request is to remove the validator\n\t\t\t\tremoveValidator(change.Address)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// This change request is to add the validator\n\t\t\taddValidator(change)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, callback, \"\")\n\n\treturn dao.NewProposalRequest(title, desc.String(), e)\n}\n\n// IsValidator returns a flag indicating if the given bech32 address\n// is part of the validator set\nfunc IsValidator(addr address) bool {\n\treturn vp.IsValidator(addr)\n}\n\n// GetValidator returns the typed validator\nfunc GetValidator(addr address) validators.Validator {\n\tif validator, err := vp.GetValidator(addr); err == nil {\n\t\treturn validator\n\t}\n\n\tpanic(\"validator not found\")\n}\n\n// GetValidators returns the typed validator set\nfunc GetValidators() []validators.Validator {\n\treturn vp.GetValidators()\n}\n"},{"name":"validators.gno","body":"package validators\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar (\n\tvp      validators.ValsetProtocol // p is the underlying validator set protocol\n\tchanges *bptree.BPTree            // changes holds any valset changes; seqid(block number) -\u003e []change\n)\n\n// change represents a single valset change, tied to a specific block number\ntype change struct {\n\tblockNum  int64                // the block number associated with the valset change\n\tvalidator validators.Validator // the validator update\n}\n\n// addValidator adds a new validator to the validator set.\n// If the validator is already present, the method errors out\nfunc addValidator(validator validators.Validator) {\n\tval, err := vp.AddValidator(validator.Address, validator.PubKey, validator.VotingPower)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator added, note the change\n\tch := change{\n\t\tblockNum:  runtime.ChainHeight(),\n\t\tvalidator: val,\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tchain.Emit(validators.ValidatorAddedEvent)\n}\n\n// removeValidator removes the given validator from the set.\n// If the validator is not present in the set, the method errors out\nfunc removeValidator(address_XXX address) {\n\tval, err := vp.RemoveValidator(address_XXX)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator removed, note the change\n\tch := change{\n\t\tblockNum: runtime.ChainHeight(),\n\t\tvalidator: validators.Validator{\n\t\t\tAddress:     val.Address,\n\t\t\tPubKey:      val.PubKey,\n\t\t\tVotingPower: 0, // nullified the voting power indicates removal\n\t\t},\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tchain.Emit(validators.ValidatorRemovedEvent)\n}\n\n// saveChange saves the valset change\nfunc saveChange(ch change) {\n\tid := getBlockID(ch.blockNum)\n\n\tsetRaw := changes.Get(id)\n\tif setRaw == nil {\n\t\tchanges.Set(id, []change{ch})\n\n\t\treturn\n\t}\n\n\t// Save the change\n\tset := setRaw.([]change)\n\tset = append(set, ch)\n\n\tchanges.Set(id, set)\n}\n\n// getBlockID converts the block number to a sequential ID\nfunc getBlockID(blockNum int64) string {\n\treturn seqid.ID(uint64(blockNum)).String()\n}\n\nfunc Render(_ string) string {\n\tvar (\n\t\tsize       = changes.Size()\n\t\tmaxDisplay = 10\n\t)\n\n\tif size == 0 {\n\t\treturn \"No valset changes to apply.\"\n\t}\n\n\toutput := \"Valset changes:\\n\"\n\tchanges.ReverseIterateByOffset(0, maxDisplay, func(_ string, value any) bool {\n\t\tchs := value.([]change)\n\n\t\tfor _, ch := range chs {\n\t\t\toutput += ufmt.Sprintf(\n\t\t\t\t\"- #%d: %s (%d)\\n\",\n\t\t\t\tch.blockNum,\n\t\t\t\tch.validator.Address.String(),\n\t\t\t\tch.validator.VotingPower,\n\t\t\t)\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn output\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"curchain_b","path":"gno.land/r/tests/vm/curchain_b","files":[{"name":"curchain_b.gno","body":"// Package curchain_b is the terminal hop in the multi-level cross-call\n// chain test helpers. It prints the full captured chain visible from this\n// realm, and asserts parity with unsafe.CurrentRealm() /\n// unsafe.PreviousRealm() at the cross-callee position.\npackage curchain_b\n\nimport (\n\t\"chain/runtime/unsafe\"\n)\n\n// B is a crossing function that prints the captured chain reached via\n// .Previous() walks and asserts parity with unsafe.CurrentRealm() /\n// unsafe.PreviousRealm() at the cross-callee position. The chain-root\n// case is covered separately by zrealm_cur_parity.\n//\n// Chain layout for callsite main -\u003e curchain_a.A -\u003e curchain_b.B:\n//\n//\tcur                  = B\n//\tcur.Previous()       = A\n//\tcur.Previous()^2     = main\n//\tcur.Previous()^3     = EOA origin (PkgPath() == \"\")\n//\tcur.Previous()^4     = panics (origin has no further previous)\nfunc B(cur realm) {\n\tprintln(\"B cur.PkgPath:\", cur.PkgPath())\n\tp1 := cur.Previous()\n\tprintln(\"B Previous().PkgPath:\", p1.PkgPath())\n\tp2 := p1.Previous()\n\tprintln(\"B Previous().Previous().PkgPath:\", p2.PkgPath())\n\tp3 := p2.Previous()\n\tprintln(\"B Previous()^3 PkgPath empty (EOA origin):\", p3.PkgPath() == \"\")\n\tfunc() {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tprintln(\"B Previous()^4 panics past origin:\", r != nil)\n\t\t}()\n\t\t_ = p3.Previous()\n\t}()\n\n\t// Parity at cross-callee position: cur agrees with\n\t// unsafe.CurrentRealm(), and cur.Previous() (the caller realm\n\t// A) agrees with unsafe.PreviousRealm().\n\trc := unsafe.CurrentRealm()\n\trp := unsafe.PreviousRealm()\n\tprintln(\"B parity cur==Current:\",\n\t\tstring(cur.Address()) == string(rc.Address()) \u0026\u0026\n\t\t\tcur.PkgPath() == rc.PkgPath())\n\tprintln(\"B parity prev==Previous:\",\n\t\tstring(p1.Address()) == string(rp.Address()) \u0026\u0026\n\t\t\tp1.PkgPath() == rp.PkgPath())\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/curchain_b\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"curchain_a","path":"gno.land/r/tests/vm/curchain_a","files":[{"name":"curchain_a.gno","body":"// Package curchain_a is a test helper for multi-level cross-call chain tests.\n// A is the middle hop: when called via cross, it cross-calls into curchain_b.\npackage curchain_a\n\nimport (\n\t\"gno.land/r/tests/vm/curchain_b\"\n)\n\n// A is a crossing function that cross-calls curchain_b.B(cross).\n// Use from a filetest as: curchain_a.A(cross) to set up a 3-level chain\n// caller → A → B with the filetest's main as the chain root.\nfunc A(cur realm) {\n\tprintln(\"A cur.PkgPath:\", cur.PkgPath())\n\tprintln(\"A prev.PkgPath:\", cur.Previous().PkgPath())\n\tcurchain_b.B(cross(cur))\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/curchain_a\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"innerowner","path":"gno.land/r/tests/vm/innerowner","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/innerowner\"\ngno = \"0.9\"\n"},{"name":"innerowner.gno","body":"// Package innerowner persists an *Inner that lives in this realm.\n// Foreign-realm callers use GetInner() to obtain a pointer to it.\n// When they pass it as the receiver of a /p/ method, PushFrameCall's\n// borrow rule 2 fires and m.Realm shifts to this realm.\npackage innerowner\n\nimport \"gno.land/p/demo/tests/p_closurecap\"\n\nvar inn *p_closurecap.Inner\n\nfunc init() {\n\tinn = \u0026p_closurecap.Inner{N: 0}\n}\n\n// GetInner returns the persisted Inner. Non-crossing: the caller's\n// realm stays current, but the returned *Inner carries PkgID =\n// innerowner from its init-time allocation.\nfunc GetInner() *p_closurecap.Inner {\n\treturn inn\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"blog","path":"gno.land/p/demo/blog","files":[{"name":"blog.gno","body":"package blog\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Blog struct {\n\tTitle             string\n\tPrefix            string   // i.e. r/gnoland/blog:\n\tPosts             avl.Tree // slug -\u003e *Post\n\tPostsPublished    avl.Tree // published-date -\u003e *Post\n\tPostsAlphabetical avl.Tree // title -\u003e *Post\n\tNoBreadcrumb      bool\n}\n\nfunc (b Blog) RenderLastPostsWidget(limit int) string {\n\tif b.PostsPublished.Size() == 0 {\n\t\treturn \"No posts.\"\n\t}\n\n\toutput := \"\"\n\ti := 0\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tp := value.(*Post)\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", p.Title, p.URL())\n\t\ti++\n\t\treturn i \u003e= limit\n\t})\n\treturn output\n}\n\nfunc (b Blog) RenderHome(res *mux.ResponseWriter, _ *mux.Request) {\n\tif !b.NoBreadcrumb {\n\t\tres.Write(breadcrumb([]string{b.Title}))\n\t}\n\n\tif b.Posts.Size() == 0 {\n\t\tres.Write(\"No posts.\")\n\t\treturn\n\t}\n\n\tconst maxCol = 3\n\tvar rowItems []string\n\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tpost := value.(*Post)\n\t\trowItems = append(rowItems, post.RenderListItem())\n\n\t\tif len(rowItems) == maxCol {\n\t\t\tres.Write(\"\u003cgno-columns\u003e\" + strings.Join(rowItems, \"\u003cgno-columns-sep\u003e\") + \"\u003c/gno-columns\u003e\\n\")\n\t\t\trowItems = []string{}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Pad and flush any remaining items\n\tif len(rowItems) \u003e 0 {\n\t\tfor len(rowItems) \u003c maxCol {\n\t\t\trowItems = append(rowItems, \"\")\n\t\t}\n\t\tres.Write(\"\u003cgno-columns\u003e\" + strings.Join(rowItems, \"\\n\u003cgno-columns-sep\u003e\\n\") + \"\u003c/gno-columns\u003e\\n\")\n\t}\n}\n\nfunc (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) {\n\tslug := req.GetVar(\"slug\")\n\n\tpost := b.Posts.Get(slug)\n\tif post == nil {\n\t\tres.Write(\"404\")\n\t\treturn\n\t}\n\tp := post.(*Post)\n\n\tres.Write(\"\u003cmain class='gno-tmpl-page'\u003e\" + \"\\n\\n\")\n\n\tres.Write(\"# \" + p.Title + \"\\n\\n\")\n\tres.Write(p.Body + \"\\n\\n\")\n\tres.Write(\"---\\n\\n\")\n\n\tres.Write(p.RenderTagList() + \"\\n\\n\")\n\tres.Write(p.RenderAuthorList() + \"\\n\\n\")\n\tres.Write(p.RenderPublishData() + \"\\n\\n\")\n\n\tres.Write(\"---\\n\")\n\tres.Write(\"\u003cdetails\u003e\u003csummary\u003eComment section\u003c/summary\u003e\\n\\n\")\n\n\t// comments\n\tp.Comments.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tcomment := value.(*Comment)\n\t\tres.Write(comment.RenderListItem())\n\t\treturn false\n\t})\n\n\tres.Write(\"\u003c/details\u003e\\n\")\n\tres.Write(\"\u003c/main\u003e\")\n}\n\nfunc (b Blog) RenderTag(res *mux.ResponseWriter, req *mux.Request) {\n\tslug := req.GetVar(\"slug\")\n\n\tif slug == \"\" {\n\t\tres.Write(\"404\")\n\t\treturn\n\t}\n\n\tif !b.NoBreadcrumb {\n\t\tbreadStr := breadcrumb([]string{\n\t\t\tufmt.Sprintf(\"[%s](%s)\", b.Title, b.Prefix),\n\t\t\t\"t\",\n\t\t\tslug,\n\t\t})\n\t\tres.Write(breadStr)\n\t}\n\n\tnb := 0\n\tb.Posts.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tif !post.HasTag(slug) {\n\t\t\treturn false\n\t\t}\n\t\tres.Write(post.RenderListItem())\n\t\tnb++\n\t\treturn false\n\t})\n\tif nb == 0 {\n\t\tres.Write(\"No posts.\")\n\t}\n}\n\nfunc (b Blog) Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", b.RenderHome)\n\trouter.HandleFunc(\"p/{slug}\", b.RenderPost)\n\trouter.HandleFunc(\"t/{slug}\", b.RenderTag)\n\treturn router.Render(path)\n}\n\nfunc (b *Blog) NewPost(publisher address, slug, title, body, pubDate string, authors, tags []string) error {\n\tif b.Posts.Has(slug) {\n\t\treturn ErrPostSlugExists\n\t}\n\n\tvar parsedTime time.Time\n\tvar err error\n\tif pubDate != \"\" {\n\t\tparsedTime, err = time.Parse(time.RFC3339, pubDate)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// If no publication date was passed in by caller, take current block time\n\t\tparsedTime = time.Now()\n\t}\n\n\tpost := \u0026Post{\n\t\tPublisher: publisher,\n\t\tAuthors:   authors,\n\t\tSlug:      slug,\n\t\tTitle:     title,\n\t\tBody:      body,\n\t\tTags:      tags,\n\t\tCreatedAt: parsedTime,\n\t}\n\n\treturn b.prepareAndSetPost(post, false)\n}\n\nfunc (b *Blog) prepareAndSetPost(post *Post, edit bool) error {\n\tpost.Title = strings.TrimSpace(post.Title)\n\tpost.Body = strings.TrimSpace(post.Body)\n\n\tif post.Title == \"\" {\n\t\treturn ErrPostTitleMissing\n\t}\n\tif post.Body == \"\" {\n\t\treturn ErrPostBodyMissing\n\t}\n\tif post.Slug == \"\" {\n\t\treturn ErrPostSlugMissing\n\t}\n\n\tpost.Blog = b\n\tpost.UpdatedAt = time.Now()\n\n\ttrimmedTitleKey := getTitleKey(post.Title)\n\tpubDateKey := getPublishedKey(post.CreatedAt)\n\n\tif !edit {\n\t\t// Cannot have two posts with same title key\n\t\tif b.PostsAlphabetical.Has(trimmedTitleKey) {\n\t\t\treturn ErrPostTitleExists\n\t\t}\n\t\t// Cannot have two posts with *exact* same timestamp\n\t\tif b.PostsPublished.Has(pubDateKey) {\n\t\t\treturn ErrPostPubDateExists\n\t\t}\n\t}\n\n\t// Store post under keys\n\tb.PostsAlphabetical.Set(trimmedTitleKey, post)\n\tb.PostsPublished.Set(pubDateKey, post)\n\tb.Posts.Set(post.Slug, post)\n\n\treturn nil\n}\n\nfunc (b *Blog) RemovePost(slug string) {\n\tp := b.Posts.Get(slug)\n\tif p == nil {\n\t\tpanic(\"post with specified slug doesn't exist\")\n\t}\n\n\tpost := p.(*Post)\n\n\ttitleKey := getTitleKey(post.Title)\n\tpublishedKey := getPublishedKey(post.CreatedAt)\n\n\t_, _ = b.Posts.Remove(slug)\n\t_, _ = b.PostsAlphabetical.Remove(titleKey)\n\t_, _ = b.PostsPublished.Remove(publishedKey)\n}\n\nfunc (b *Blog) GetPost(slug string) *Post {\n\tpost := b.Posts.Get(slug)\n\tif post == nil {\n\t\treturn nil\n\t}\n\treturn post.(*Post)\n}\n\ntype Post struct {\n\tBlog         *Blog\n\tSlug         string // FIXME: save space?\n\tTitle        string\n\tBody         string\n\tCreatedAt    time.Time\n\tUpdatedAt    time.Time\n\tComments     avl.Tree\n\tAuthors      []string\n\tPublisher    address\n\tTags         []string\n\tCommentIndex int\n}\n\nfunc (p *Post) Update(title, body, publicationDate string, authors, tags []string) error {\n\tp.Title = title\n\tp.Body = body\n\tp.Tags = tags\n\tp.Authors = authors\n\n\tparsedTime, err := time.Parse(time.RFC3339, publicationDate)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.CreatedAt = parsedTime\n\treturn p.Blog.prepareAndSetPost(p, true)\n}\n\nfunc (p *Post) AddComment(author address, comment string) error {\n\tif p == nil {\n\t\treturn ErrNoSuchPost\n\t}\n\tp.CommentIndex++\n\tcommentKey := strconv.Itoa(p.CommentIndex)\n\tcomment = strings.TrimSpace(comment)\n\tp.Comments.Set(commentKey, \u0026Comment{\n\t\tPost:      p,\n\t\tCreatedAt: time.Now(),\n\t\tAuthor:    author,\n\t\tComment:   comment,\n\t})\n\n\treturn nil\n}\n\nfunc (p *Post) DeleteComment(index int) error {\n\tif p == nil {\n\t\treturn ErrNoSuchPost\n\t}\n\tcommentKey := strconv.Itoa(index)\n\tp.Comments.Remove(commentKey)\n\treturn nil\n}\n\nfunc (p *Post) HasTag(tag string) bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\tfor _, t := range p.Tags {\n\t\tif t == tag {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (p *Post) RenderListItem() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\toutput := ufmt.Sprintf(\"\\n### [%s](%s)\\n\", p.Title, p.URL())\n\t// output += ufmt.Sprintf(\"**[Learn More](%s)**\\n\\n\", p.URL())\n\n\toutput += p.CreatedAt.Format(\"02 Jan 2006\")\n\t// output += p.Summary() + \"\\n\\n\"\n\t// output += p.RenderTagList() + \"\\n\\n\"\n\toutput += \"\\n\"\n\treturn output\n}\n\n// Render post tags\nfunc (p *Post) RenderTagList() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\tif len(p.Tags) == 0 {\n\t\treturn \"\"\n\t}\n\n\toutput := \"Tags: \"\n\tfor idx, tag := range p.Tags {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" \"\n\t\t}\n\t\ttagURL := p.Blog.Prefix + \"t/\" + tag\n\t\toutput += ufmt.Sprintf(\"[#%s](%s)\", tag, tagURL)\n\n\t}\n\treturn output\n}\n\n// Render authors if there are any\nfunc (p *Post) RenderAuthorList() string {\n\tout := \"Written\"\n\tif len(p.Authors) != 0 {\n\t\tout += \" by \"\n\n\t\tfor idx, author := range p.Authors {\n\t\t\tout += author\n\t\t\tif idx \u003c len(p.Authors)-1 {\n\t\t\t\tout += \", \"\n\t\t\t}\n\t\t}\n\t}\n\tout += \" on \" + p.CreatedAt.Format(\"02 Jan 2006\")\n\n\treturn out\n}\n\nfunc (p *Post) RenderPublishData() string {\n\tout := \"Published \"\n\tif p.Publisher != \"\" {\n\t\tout += \"by \" + p.Publisher.String() + \" \"\n\t}\n\tout += \"to \" + p.Blog.Title\n\n\treturn out\n}\n\nfunc (p *Post) URL() string {\n\tif p == nil {\n\t\treturn p.Blog.Prefix + \"404\"\n\t}\n\treturn p.Blog.Prefix + \"p/\" + p.Slug\n}\n\nfunc (p *Post) Summary() string {\n\tif p == nil {\n\t\treturn \"error: no such post\\n\"\n\t}\n\n\t// FIXME: better summary.\n\tlines := strings.Split(p.Body, \"\\n\")\n\tif len(lines) \u003c= 3 {\n\t\treturn p.Body\n\t}\n\treturn strings.Join(lines[0:3], \"\\n\") + \"...\"\n}\n\ntype Comment struct {\n\tPost      *Post\n\tCreatedAt time.Time\n\tAuthor    address\n\tComment   string\n}\n\nfunc (c Comment) RenderListItem() string {\n\toutput := \"\u003ch5\u003e\"\n\toutput += c.Comment + \"\\n\\n\"\n\toutput += \"\u003c/h5\u003e\"\n\n\toutput += \"\u003ch6\u003e\"\n\toutput += ufmt.Sprintf(\"by %s on %s\", c.Author, c.CreatedAt.Format(time.RFC822))\n\toutput += \"\u003c/h6\u003e\\n\\n\"\n\n\toutput += \"---\\n\\n\"\n\n\treturn output\n}\n"},{"name":"errors.gno","body":"package blog\n\nimport \"errors\"\n\nvar (\n\tErrPostTitleMissing  = errors.New(\"post title is missing\")\n\tErrPostSlugMissing   = errors.New(\"post slug is missing\")\n\tErrPostBodyMissing   = errors.New(\"post body is missing\")\n\tErrPostSlugExists    = errors.New(\"post with specified slug already exists\")\n\tErrPostPubDateExists = errors.New(\"post with specified publication date exists\")\n\tErrPostTitleExists   = errors.New(\"post with specified title already exists\")\n\tErrNoSuchPost        = errors.New(\"no such post\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/blog\"\ngno = \"0.9\"\n"},{"name":"util.gno","body":"package blog\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\nfunc breadcrumb(parts []string) string {\n\treturn \"# \" + strings.Join(parts, \" / \") + \"\\n\\n\"\n}\n\nfunc getTitleKey(title string) string {\n\treturn strings.ReplaceAll(title, \" \", \"\")\n}\n\nfunc getPublishedKey(t time.Time) string {\n\treturn t.Format(time.RFC3339)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"agent","path":"gno.land/p/demo/gnorkle/agent","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/agent\"\ngno = \"0.9\"\n"},{"name":"whitelist.gno","body":"package agent\n\nimport \"gno.land/p/nt/avl/v0\"\n\n// Whitelist manages whitelisted agent addresses.\ntype Whitelist struct {\n\tstore *avl.Tree\n}\n\n// ClearAddresses removes all addresses from the whitelist and puts into a state\n// that indicates it is moot and has no whitelist defined.\nfunc (m *Whitelist) ClearAddresses() {\n\tm.store = nil\n}\n\n// AddAddresses adds the given addresses to the whitelist.\nfunc (m *Whitelist) AddAddresses(addresses []string) {\n\tif m.store == nil {\n\t\tm.store = avl.NewTree()\n\t}\n\n\tfor _, address_XXX := range addresses {\n\t\tm.store.Set(address_XXX, struct{}{})\n\t}\n}\n\n// RemoveAddress removes the given address from the whitelist if it exists.\nfunc (m *Whitelist) RemoveAddress(address_XXX string) {\n\tif m.store == nil {\n\t\treturn\n\t}\n\n\tm.store.Remove(address_XXX)\n}\n\n// HasDefinition returns true if the whitelist has a definition. It retuns false if\n// `ClearAddresses` has been called without any subsequent `AddAddresses` calls, or\n// if `AddAddresses` has never been called.\nfunc (m Whitelist) HasDefinition() bool {\n\treturn m.store != nil\n}\n\n// HasAddress returns true if the given address is in the whitelist.\nfunc (m Whitelist) HasAddress(address_XXX string) bool {\n\tif m.store == nil {\n\t\treturn false\n\t}\n\n\treturn m.store.Has(address_XXX)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"expect","path":"gno.land/p/jeronimoalbi/expect","files":[{"name":"boolean.gno","body":"package expect\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewBooleanChecker creates a new checker of boolean values\nfunc NewBooleanChecker(ctx Context, value bool) BooleanChecker {\n\treturn BooleanChecker{ctx, value}\n}\n\n// BooleanChecker asserts boolean values.\ntype BooleanChecker struct {\n\tctx   Context\n\tvalue bool\n}\n\n// Not negates the next called expectation.\nfunc (c BooleanChecker) Not() BooleanChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c BooleanChecker) ToEqual(v bool) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == v, func(ctx Context) string {\n\t\tgot := formatBoolean(c.value)\n\t\tif !ctx.IsNegated() {\n\t\t\twant := formatBoolean(v)\n\t\t\treturn ufmt.Sprintf(\"Expected values to match\\nGot: %s\\nWant: %s\", got, want)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected values to be different\\nGot: %s\", got)\n\t})\n}\n\n// ToBeFalsy asserts that current value is falsy.\nfunc (c BooleanChecker) ToBeFalsy() {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(!c.value, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected value to be falsy\"\n\t\t}\n\t\treturn \"Expected value not to be falsy\"\n\t})\n}\n\n// ToBeTruthy asserts that current value is truthy.\nfunc (c BooleanChecker) ToBeTruthy() {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected value to be truthy\"\n\t\t}\n\t\treturn \"Expected value not to be truthy\"\n\t})\n}\n\nfunc asBoolean(value any) (bool, error) {\n\tif value == nil {\n\t\treturn false, nil\n\t}\n\n\tvar s string\n\tswitch v := value.(type) {\n\tcase bool:\n\t\treturn v, nil\n\tcase string:\n\t\ts = v\n\tcase []byte:\n\t\ts = string(v)\n\tcase Stringer:\n\t\ts = v.String()\n\tdefault:\n\t\treturn false, ErrIncompatibleType\n\t}\n\n\tif s != \"\" {\n\t\treturn strconv.ParseBool(s)\n\t}\n\treturn false, nil\n}\n\nfunc formatBoolean(value bool) string {\n\treturn strconv.FormatBool(value)\n}\n"},{"name":"context.gno","body":"package expect\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst defaultAssertFailMsg = \"assert expectation failed\"\n\n// NewContext creates a new testing context.\nfunc NewContext(t TestingT) Context {\n\treturn Context{t: t}\n}\n\n// Context preserves the current testing context.\ntype Context struct {\n\tt       TestingT\n\tnegated bool\n\tprefix  string\n}\n\n// T returns context's testing T instance.\nfunc (c Context) T() TestingT {\n\tif c.t == nil {\n\t\tpanic(\"expect: context is not initialized\")\n\t}\n\treturn c.t\n}\n\n// Prefix returns context's error prefix.\nfunc (c Context) Prefix() string {\n\treturn c.prefix\n}\n\n// IsNegated checks if current context negates current assert expectations.\nfunc (c Context) IsNegated() bool {\n\treturn c.negated\n}\n\n// CheckExpectation checks an assert expectation and calls a callback on fail.\n// It returns true when the asserted expectation fails.\n// Callback is called when a negated assertion succeeds or when non negated assertion fails.\nfunc (c Context) CheckExpectation(success bool, cb func(Context) string) bool {\n\tfailed := (c.negated \u0026\u0026 success) || (!c.negated \u0026\u0026 !success)\n\tif failed {\n\t\tmsg := cb(c)\n\t\tif strings.TrimSpace(msg) == \"\" {\n\t\t\tmsg = defaultAssertFailMsg\n\t\t}\n\n\t\tc.Fail(msg)\n\t}\n\treturn failed\n}\n\n// Fail makes the current test fail with a custom message.\nfunc (c Context) Fail(msg string, args ...any) {\n\tif c.prefix != \"\" {\n\t\tmsg = c.prefix + \" - \" + msg\n\t}\n\n\tc.t.Fatalf(msg, args...)\n}\n\n// TestingT defines a minimal interface for `testing.T` instances.\ntype TestingT interface {\n\tHelper()\n\tFatal(args ...any)\n\tFatalf(format string, args ...any)\n}\n\n// MockTestingT creates a new testing mock that writes testing output to a string builder.\nfunc MockTestingT(output *strings.Builder) TestingT {\n\treturn \u0026testingT{output}\n}\n\ntype testingT struct{ buf *strings.Builder }\n\nfunc (testingT) Helper()                          {}\nfunc (t testingT) Fatal(args ...any)              { t.buf.WriteString(ufmt.Sprintln(args...)) }\nfunc (t testingT) Fatalf(fmt string, args ...any) { t.buf.WriteString(ufmt.Sprintf(fmt+\"\\n\", args...)) }\n"},{"name":"doc.gno","body":"// Package expect provides testing support for packages and realms.\n//\n// The opinionated approach taken on this package for testing is to use function chaining and\n// semanthics to hopefully make unit and file testing fun. Focus is not on speed as there are\n// other packages that would run tests faster like the official `uassert` or `urequire` packages.\n//\n// Values can be asserted using the `Value()` function, for example:\n//\n//\tfunc TestFoo(t *testing.T) {\n//\t  got := 42\n//\t  expect.Value(t, got).ToEqual(42)\n//\t  expect.Value(t, got).Not().ToEqual(0)\n//\n//\t  expect.Value(t, \"foo\").ToEqual(\"foo\")\n//\t  expect.Value(t, 42).AsInt().Not().ToBeGreaterThan(50)\n//\t  expect.Value(t, \"TRUE\").AsBoolean().ToBeTruthy()\n//\t}\n//\n// Functions can also be used to assert returned values, errors or panics.\n//\n// Package supports four type of functions:\n//\n//   - func()\n//   - func() any\n//   - func() error\n//   - func() (any, error)\n//\n// Functions can be asserted using the `Func()` function, for example:\n//\n//\tfunc TestFoo(t *testing.T) {\n//\t  expect.Func(t, func() {\n//\t    panic(\"Boom!\")\n//\t  }).ToPanic().WithMessage(\"Boom!\")\n//\n//\t  wantErr := errors.New(\"Boom!\")\n//\t  expect.Func(t, func() error {\n//\t    return wantErr\n//\t  }).ToFail().WithMessage(\"Boom!\")\n//\n//\t  expect.Func(t, func() error {\n//\t    return wantErr\n//\t  }).ToFail().WithError(wantErr)\n//\t}\npackage expect\n"},{"name":"error.gno","body":"package expect\n\nimport \"gno.land/p/nt/ufmt/v0\"\n\n// NewErrorChecker creates a new checker of errors.\nfunc NewErrorChecker(ctx Context, err error) ErrorChecker {\n\treturn ErrorChecker{ctx, err}\n}\n\n// ErrorChecker asserts error values.\ntype ErrorChecker struct {\n\tctx Context\n\terr error\n}\n\n// Not negates the next called expectation.\nfunc (c ErrorChecker) Not() ErrorChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// WithMessage asserts that current error contains an expected message.\nfunc (c ErrorChecker) WithMessage(msg string) {\n\tc.ctx.T().Helper()\n\n\tif c.err == nil {\n\t\tc.ctx.Fail(\"Expected an error with message\\nGot: nil\\nWant: %s\", msg)\n\t\treturn\n\t}\n\n\tNewMessageChecker(c.ctx, c.err.Error(), MessageTypeError).WithMessage(msg)\n}\n\n// WithError asserts that current error message is the same as an expected error.\nfunc (c ErrorChecker) WithError(err error) {\n\tc.ctx.T().Helper()\n\n\tif c.err == nil {\n\t\tif err != nil {\n\t\t\tc.ctx.Fail(\"Expected an error\\nGot: nil\\nWant: %s\", err.Error())\n\t\t}\n\t\treturn\n\t}\n\n\tgot := c.err.Error()\n\tc.ctx.CheckExpectation(got == err.Error(), func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected errors to match\\nGot: %s\\nWant: %s\", got, err.Error())\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected errors to be different\\nGot: %s\", got)\n\t})\n}\n"},{"name":"float.gno","body":"package expect\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewFloatChecker creates a new checker of float64 values.\nfunc NewFloatChecker(ctx Context, value float64) FloatChecker {\n\treturn FloatChecker{ctx, value}\n}\n\n// FloatChecker asserts float64 values.\ntype FloatChecker struct {\n\tctx   Context\n\tvalue float64\n}\n\n// Not negates the next called expectation.\nfunc (c FloatChecker) Not() FloatChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c FloatChecker) ToEqual(value float64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == value, func(ctx Context) string {\n\t\tgot := formatFloat(c.value)\n\t\tif !ctx.IsNegated() {\n\t\t\twant := formatFloat(value)\n\t\t\treturn ufmt.Sprintf(\"Expected values to match\\nGot: %s\\nWant: %s\", got, want)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to be different\\nGot: %s\", got)\n\t})\n}\n\n// ToBeGreaterThan asserts that current value is greater than an expected value.\nfunc (c FloatChecker) ToBeGreaterThan(value float64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003e value, func(ctx Context) string {\n\t\tgot := formatFloat(c.value)\n\t\twant := formatFloat(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be gerater than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be greater than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeGreaterOrEqualThan asserts that current value is greater or equal than an expected value.\nfunc (c FloatChecker) ToBeGreaterOrEqualThan(value float64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003e= value, func(ctx Context) string {\n\t\tgot := formatFloat(c.value)\n\t\twant := formatFloat(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be greater or equal than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be greater or equal than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeLowerThan asserts that current value is lower than an expected value.\nfunc (c FloatChecker) ToBeLowerThan(value float64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003c value, func(ctx Context) string {\n\t\tgot := formatFloat(c.value)\n\t\twant := formatFloat(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be lower than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be lower than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeLowerOrEqualThan asserts that current value is lower or equal than an expected value.\nfunc (c FloatChecker) ToBeLowerOrEqualThan(value float64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003c= value, func(ctx Context) string {\n\t\tgot := formatFloat(c.value)\n\t\twant := formatFloat(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be lower or equal than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be lower or equal than %s\\nGot: %s\", want, got)\n\t})\n}\n\nfunc formatFloat(value float64) string {\n\treturn strconv.FormatFloat(value, 'g', -1, 64)\n}\n\nfunc asFloat(value any) (float64, error) {\n\tswitch v := value.(type) {\n\tcase float32:\n\t\treturn float64(v), nil\n\tcase float64:\n\t\treturn v, nil\n\tdefault:\n\t\treturn 0, ErrIncompatibleType\n\t}\n}\n"},{"name":"func.gno","body":"package expect\n\nimport \"gno.land/p/nt/ufmt/v0\"\n\ntype (\n\t// Fn defines a type for generic functions.\n\tFn = func()\n\n\t// ErrorFn defines a type for generic functions that return an error.\n\tErrorFn = func() error\n\n\t// AnyFn defines a type for generic functions that returns a value.\n\tAnyFn = func() any\n\n\t// AnyErrorFn defines a type for generic functions that return a value and an error.\n\tAnyErrorFn = func() (any, error)\n)\n\n// Func creates a new checker for functions.\nfunc Func(t TestingT, fn any) FuncChecker {\n\treturn FuncChecker{\n\t\tctx: NewContext(t),\n\t\tfn:  fn,\n\t}\n}\n\n// FuncChecker asserts function panics, errors and returned value.\ntype FuncChecker struct {\n\tctx Context\n\tfn  any\n}\n\n// WithFailPrefix assigns a prefix that will be prefixed to testing errors when an assertion fails.\nfunc (c FuncChecker) WithFailPrefix(prefix string) FuncChecker {\n\tc.ctx.prefix = prefix\n\treturn c\n}\n\n// Not negates the next called expectation.\nfunc (c FuncChecker) Not() FuncChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToFail return an error checker to assert if current function returns an error.\nfunc (c FuncChecker) ToFail() ErrorChecker {\n\tc.ctx.T().Helper()\n\n\tvar err error\n\tswitch fn := c.fn.(type) {\n\tcase ErrorFn:\n\t\terr = fn()\n\tcase AnyErrorFn:\n\t\t_, err = fn()\n\tdefault:\n\t\tc.ctx.Fail(\"Unsupported error func type\\nGot: %T\", c.fn)\n\t\treturn ErrorChecker{}\n\t}\n\n\tc.ctx.CheckExpectation(err != nil, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected func to return an error\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"Func failed with error\\nGot: %s\", err.Error())\n\t})\n\n\treturn NewErrorChecker(c.ctx, err)\n}\n\n// ToPanic return an message checker to assert if current function panicked.\n// This assertion is handled within the same realm, to assert panics when crossing\n// to another realm use the `ToAbort()` assertion.\n//\n// Example usage:\n//\n//\tfunc TestFoo(t *testing.T) {\n//\t  expect.Func(t, func() {\n//\t    Foo(cross)\n//\t  }).Not().ToCrossPanic()\n//\t}\nfunc (c FuncChecker) ToPanic() MessageChecker {\n\tc.ctx.T().Helper()\n\n\tvar (\n\t\tmsg      string\n\t\tpanicked bool\n\t)\n\n\t// TODO: Can't use a switch because it triggers the following VM error:\n\t// \"panic: should not happen, should be heapItemType: fn\u003c()~VPBlock(1,0)\u003e\"\n\t//\n\t// switch fn := c.fn.(type) {\n\t// case Fn:\n\t// \tmsg, panicked = handlePanic(fn)\n\t// case ErrorFn:\n\t// \tmsg, panicked = handlePanic(func() { _ = fn() })\n\t// case AnyFn:\n\t// \tmsg, panicked = handlePanic(func() { _ = fn() })\n\t// case AnyErrorFn:\n\t// \tmsg, panicked = handlePanic(func() { _, _ = fn() })\n\t// default:\n\t// \tc.ctx.Fail(\"Unsupported func type\\nGot: %T\", c.fn)\n\t// \treturn MessageChecker{}\n\t// }\n\n\tif fn, ok := c.fn.(Fn); ok {\n\t\tmsg, panicked = handlePanic(fn)\n\t} else if fn, ok := c.fn.(ErrorFn); ok {\n\t\tmsg, panicked = handlePanic(func() { _ = fn() })\n\t} else if fn, ok := c.fn.(AnyFn); ok {\n\t\tmsg, panicked = handlePanic(func() { _ = fn() })\n\t} else if fn, ok := c.fn.(AnyErrorFn); ok {\n\t\tmsg, panicked = handlePanic(func() { _, _ = fn() })\n\t} else {\n\t\tc.ctx.Fail(\"Unsupported func type\\nGot: %T\", c.fn)\n\t\treturn MessageChecker{}\n\t}\n\n\tc.ctx.CheckExpectation(panicked, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected function to panic\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected func not to panic\\nGot: %s\", msg)\n\t})\n\n\treturn NewMessageChecker(c.ctx, msg, MessageTypePanic)\n}\n\n// ToCrossPanic return an message checker to assert if current function panicked when crossing.\n// This assertion is handled only when making a crossing call to another realm, when asserting\n// within the same realm use `ToPanic()`.\nfunc (c FuncChecker) ToCrossPanic() MessageChecker {\n\tc.ctx.T().Helper()\n\n\tvar (\n\t\tmsg      string\n\t\tpanicked bool\n\t)\n\n\t// TODO: Can't use a switch because it triggers the following VM error:\n\t// \"panic: should not happen, should be heapItemType: fn\u003c()~VPBlock(1,0)\u003e\"\n\t//\n\t// switch fn := c.fn.(type) {\n\t// case Fn:\n\t// \tmsg, panicked = handleCrossPanic(fn)\n\t// case ErrorFn:\n\t// \tmsg, panicked = handleCrossPanic(func() { _ = fn() })\n\t// case AnyFn:\n\t// \tmsg, panicked = handleCrossPanic(func() { _ = fn() })\n\t// case AnyErrorFn:\n\t// \tmsg, panicked = handleCrossPanic(func() { _, _ = fn() })\n\t// default:\n\t// \tc.ctx.Fail(\"Unsupported func type\\nGot: %T\", c.fn)\n\t// \treturn MessageChecker{}\n\t// }\n\n\tif fn, ok := c.fn.(Fn); ok {\n\t\tmsg, panicked = handleCrossPanic(fn)\n\t} else if fn, ok := c.fn.(ErrorFn); ok {\n\t\tmsg, panicked = handleCrossPanic(func() { _ = fn() })\n\t} else if fn, ok := c.fn.(AnyFn); ok {\n\t\tmsg, panicked = handleCrossPanic(func() { _ = fn() })\n\t} else if fn, ok := c.fn.(AnyErrorFn); ok {\n\t\tmsg, panicked = handleCrossPanic(func() { _, _ = fn() })\n\t} else {\n\t\tc.ctx.Fail(\"Unsupported func type\\nGot: %T\", c.fn)\n\t\treturn MessageChecker{}\n\t}\n\n\tc.ctx.CheckExpectation(panicked, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected function to cross panic\"\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected func not to cross panic\\nGot: %s\", msg)\n\t})\n\n\treturn NewMessageChecker(c.ctx, msg, MessageTypeCrossPanic)\n}\n\n// ToReturn asserts that current function returned a value equal to an expected value.\nfunc (c FuncChecker) ToReturn(value any) {\n\tc.ctx.T().Helper()\n\n\tvar (\n\t\terr error\n\t\tv   any\n\t)\n\n\tif fn, ok := c.fn.(AnyFn); ok {\n\t\tv = fn()\n\t} else if fn, ok := c.fn.(AnyErrorFn); ok {\n\t\tv, err = fn()\n\t} else {\n\t\tc.ctx.Fail(\"Unsupported func type\\nGot: %T\", c.fn)\n\t\treturn\n\t}\n\n\tif err != nil {\n\t\tc.ctx.Fail(\"Function returned unexpected error\\nGot: %s\", err.Error())\n\t\treturn\n\t}\n\n\tif c.ctx.negated {\n\t\tValue(c.ctx.T(), v).Not().ToEqual(value)\n\t} else {\n\t\tValue(c.ctx.T(), v).ToEqual(value)\n\t}\n}\n\nfunc handlePanic(fn func()) (msg string, panicked bool) {\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil {\n\t\t\treturn\n\t\t}\n\n\t\tpanicked = true\n\n\t\tif err, ok := r.(error); ok {\n\t\t\tmsg = err.Error()\n\t\t\treturn\n\t\t}\n\n\t\tif s, ok := r.(string); ok {\n\t\t\tmsg = s\n\t\t\treturn\n\t\t}\n\n\t\tmsg = \"unsupported panic type\"\n\t}()\n\n\tfn()\n\treturn\n}\n\nfunc handleCrossPanic(fn func()) (string, bool) {\n\tr := revive(fn)\n\tif r == nil {\n\t\treturn \"\", false\n\t}\n\n\tif err, ok := r.(error); ok {\n\t\treturn err.Error(), true\n\t}\n\n\tif s, ok := r.(string); ok {\n\t\treturn s, true\n\t}\n\n\treturn \"unsupported panic type\", true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/expect\"\ngno = \"0.9\"\n"},{"name":"int.gno","body":"package expect\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewIntChecker creates a new checker of int64 values.\nfunc NewIntChecker(ctx Context, value int64) IntChecker {\n\treturn IntChecker{ctx, value}\n}\n\n// IntChecker asserts int64 values.\ntype IntChecker struct {\n\tctx   Context\n\tvalue int64\n}\n\n// Not negates the next called expectation.\nfunc (c IntChecker) Not() IntChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c IntChecker) ToEqual(value int64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == value, func(ctx Context) string {\n\t\tgot := formatInt(c.value)\n\t\tif !ctx.IsNegated() {\n\t\t\twant := formatInt(value)\n\t\t\treturn ufmt.Sprintf(\"Expected values to match\\nGot: %s\\nWant: %s\", got, want)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to be different\\nGot: %s\", got)\n\t})\n}\n\n// ToBeGreaterThan asserts that current value is greater than an expected value.\nfunc (c IntChecker) ToBeGreaterThan(value int64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003e value, func(ctx Context) string {\n\t\tgot := formatInt(c.value)\n\t\twant := formatInt(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be gerater than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be greater than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeGreaterOrEqualThan asserts that current value is greater or equal than an expected value.\nfunc (c IntChecker) ToBeGreaterOrEqualThan(value int64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003e= value, func(ctx Context) string {\n\t\tgot := formatInt(c.value)\n\t\twant := formatInt(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be greater or equal than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be greater or equal than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeLowerThan asserts that current value is lower than an expected value.\nfunc (c IntChecker) ToBeLowerThan(value int64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003c value, func(ctx Context) string {\n\t\tgot := formatInt(c.value)\n\t\twant := formatInt(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be lower than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be lower than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeLowerOrEqualThan asserts that current value is lower or equal than an expected value.\nfunc (c IntChecker) ToBeLowerOrEqualThan(value int64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003c= value, func(ctx Context) string {\n\t\tgot := formatInt(c.value)\n\t\twant := formatInt(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be lower or equal than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be lower or equal than %s\\nGot: %s\", want, got)\n\t})\n}\n\nfunc formatInt(value int64) string {\n\treturn strconv.FormatInt(value, 10)\n}\n\nfunc asInt(value any) (int64, error) {\n\tswitch v := value.(type) {\n\tcase int:\n\t\treturn int64(v), nil\n\tcase int8:\n\t\treturn int64(v), nil\n\tcase int16:\n\t\treturn int64(v), nil\n\tcase int32:\n\t\treturn int64(v), nil\n\tcase int64:\n\t\treturn v, nil\n\tdefault:\n\t\treturn 0, ErrIncompatibleType\n\t}\n}\n"},{"name":"message.gno","body":"package expect\n\nimport \"gno.land/p/nt/ufmt/v0\"\n\nconst (\n\tMessageTypeCrossPanic MessageType = \"cross panic\"\n\tMessageTypeError                  = \"error\"\n\tMessageTypePanic                  = \"panic\"\n)\n\n// MessageType defines a type for message checker errors.\ntype MessageType string\n\n// NewMessageChecker creates a new checker for text messages.\nfunc NewMessageChecker(ctx Context, msg string, t MessageType) MessageChecker {\n\treturn MessageChecker{ctx, msg, t}\n}\n\n// MessageChecker asserts text messages.\ntype MessageChecker struct {\n\tctx     Context\n\tmsg     string\n\tmsgType MessageType\n}\n\n// Not negates the next called expectation.\nfunc (c MessageChecker) Not() MessageChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// WithMessage asserts that a message is equal to an expected message.\nfunc (c MessageChecker) WithMessage(msg string) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.msg == msg, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected %s message to match\\nGot: %s\\nWant: %s\", string(c.msgType), c.msg, msg)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected %s message to be different\\nGot: %s\", string(c.msgType), c.msg)\n\t})\n}\n"},{"name":"string.gno","body":"package expect\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// ErrIncompatibleType indicates that a value can't be casted to a different type.\nvar ErrIncompatibleType = errors.New(\"incompatible type\")\n\n// NewStringChecker creates a new checker of string values.\nfunc NewStringChecker(ctx Context, value string) StringChecker {\n\treturn StringChecker{ctx, value}\n}\n\n// StringChecker asserts string values.\ntype StringChecker struct {\n\tctx   Context\n\tvalue string\n}\n\n// Not negates the next called expectation.\nfunc (c StringChecker) Not() StringChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c StringChecker) ToEqual(v string) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == v, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to match\\nGot: %s\\nWant: %s\", c.value, v)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected values to be different\\nGot: %s\", c.value)\n\t})\n}\n\n// ToBeEmpty asserts that current value is an empty string.\nfunc (c StringChecker) ToBeEmpty() {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == \"\", func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected string to be empty\\nGot: %s\", c.value)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Unexpected empty string\")\n\t})\n}\n\n// ToHaveLength asserts that current value has an expected length.\nfunc (c StringChecker) ToHaveLength(length int) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(len(c.value) == length, func(ctx Context) string {\n\t\tgot := len(c.value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected string length to match\\nGot: %d\\nWant: %d\", got, length)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected string lengths to be different\\nGot: %d\", got)\n\t})\n}\n\n// Stringer defines an interface for values that has a String method.\ntype Stringer interface {\n\tString() string\n}\n\nfunc asString(value any) (string, error) {\n\tswitch v := value.(type) {\n\tcase string:\n\t\treturn v, nil\n\tcase []byte:\n\t\treturn string(v), nil\n\tcase Stringer:\n\t\treturn v.String(), nil\n\tcase address:\n\t\treturn v.String(), nil\n\tdefault:\n\t\treturn \"\", ErrIncompatibleType\n\t}\n}\n"},{"name":"uint.gno","body":"package expect\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewUintChecker creates a new checker of uint64 values.\nfunc NewUintChecker(ctx Context, value uint64) UintChecker {\n\treturn UintChecker{ctx, value}\n}\n\n// UintChecker asserts uint64 values.\ntype UintChecker struct {\n\tctx   Context\n\tvalue uint64\n}\n\n// Not negates the next called expectation.\nfunc (c UintChecker) Not() UintChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c UintChecker) ToEqual(value uint64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == value, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\tgot := formatUint(c.value)\n\t\t\twant := formatUint(value)\n\t\t\treturn ufmt.Sprintf(\"Expected values to match\\nGot: %s\\nWant: %s\", got, want)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to be different\\nGot: %s\", formatUint(c.value))\n\t})\n}\n\n// ToBeGreaterThan asserts that current value is greater than an expected value.\nfunc (c UintChecker) ToBeGreaterThan(value uint64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003e value, func(ctx Context) string {\n\t\tgot := formatUint(c.value)\n\t\twant := formatUint(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be gerater than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be greater than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeGreaterOrEqualThan asserts that current value is greater or equal than an expected value.\nfunc (c UintChecker) ToBeGreaterOrEqualThan(value uint64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003e= value, func(ctx Context) string {\n\t\tgot := formatUint(c.value)\n\t\twant := formatUint(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be greater or equal than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be greater or equal than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeLowerThan asserts that current value is lower than an expected value.\nfunc (c UintChecker) ToBeLowerThan(value uint64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003c value, func(ctx Context) string {\n\t\tgot := formatUint(c.value)\n\t\twant := formatUint(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be lower than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be lower than %s\\nGot: %s\", want, got)\n\t})\n}\n\n// ToBeLowerOrEqualThan asserts that current value is lower or equal than an expected value.\nfunc (c UintChecker) ToBeLowerOrEqualThan(value uint64) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value \u003c= value, func(ctx Context) string {\n\t\tgot := formatUint(c.value)\n\t\twant := formatUint(value)\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected values to be lower or equal than %s\\nGot: %s\", want, got)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected value to not to be lower or equal than %s\\nGot: %s\", want, got)\n\t})\n}\n\nfunc formatUint(value uint64) string {\n\treturn strconv.FormatUint(value, 10)\n}\n\nfunc asUint(value any) (uint64, error) {\n\tswitch v := value.(type) {\n\tcase uint:\n\t\treturn uint64(v), nil\n\tcase uint8:\n\t\treturn uint64(v), nil\n\tcase uint16:\n\t\treturn uint64(v), nil\n\tcase uint32:\n\t\treturn uint64(v), nil\n\tcase uint64:\n\t\treturn v, nil\n\tcase int:\n\t\tif v \u003c 0 {\n\t\t\treturn 0, ErrIncompatibleType\n\t\t}\n\t\treturn uint64(v), nil\n\tdefault:\n\t\treturn 0, ErrIncompatibleType\n\t}\n}\n"},{"name":"value.gno","body":"package expect\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Value creates a new checker values of different types.\nfunc Value(t TestingT, value any) ValueChecker {\n\treturn ValueChecker{\n\t\tctx:   NewContext(t),\n\t\tvalue: value,\n\t}\n}\n\n// ValueChecker asserts values of different types.\ntype ValueChecker struct {\n\tctx   Context\n\tvalue any\n}\n\n// WithFailPrefix assigns a prefix that will be prefixed to testing errors when an assertion fails.\nfunc (c ValueChecker) WithFailPrefix(prefix string) ValueChecker {\n\tc.ctx.prefix = prefix\n\treturn c\n}\n\n// Not negates the next called expectation.\nfunc (c ValueChecker) Not() ValueChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToBeNil asserts that current value is nil.\nfunc (c ValueChecker) ToBeNil() {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == nil || istypednil(c.value), func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected value to be nil\\nGot: %v\", c.value)\n\t\t}\n\t\treturn \"Expected a non nil value\"\n\t})\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c ValueChecker) ToEqual(value any) {\n\tc.ctx.T().Helper()\n\n\t// Assert error values first to allow comparing errors to string values\n\tif err, ok := c.value.(error); ok {\n\t\twant, ok := value.(error)\n\t\tif !ok {\n\t\t\tc.ctx.Fail(\"Failed: expected an error value\\nGot: %T\", value)\n\t\t\treturn\n\t\t}\n\n\t\tc.ctx.CheckExpectation(err.Error() == want.Error(), func(ctx Context) string {\n\t\t\tif !ctx.IsNegated() {\n\t\t\t\treturn ufmt.Sprintf(\"Expected errors to match\\nGot: %s\\nWant: %s\", err.Error(), want.Error())\n\t\t\t}\n\t\t\treturn ufmt.Sprintf(\"Expected errors to be different\\nGot: %s\", err.Error())\n\t\t})\n\n\t\treturn\n\t}\n\n\tswitch v := value.(type) {\n\tcase string:\n\t\tc.AsString().ToEqual(v)\n\tcase []byte:\n\t\tc.AsString().ToEqual(string(v))\n\tcase Stringer:\n\t\tc.AsString().ToEqual(v.String())\n\tcase bool:\n\t\tc.AsBoolean().ToEqual(v)\n\tcase float32:\n\t\tc.AsFloat().ToEqual(float64(v))\n\tcase float64:\n\t\tc.AsFloat().ToEqual(v)\n\tcase uint:\n\t\tc.AsUint().ToEqual(uint64(v))\n\tcase uint8:\n\t\tc.AsUint().ToEqual(uint64(v))\n\tcase uint16:\n\t\tc.AsUint().ToEqual(uint64(v))\n\tcase uint32:\n\t\tc.AsUint().ToEqual(uint64(v))\n\tcase uint64:\n\t\tc.AsUint().ToEqual(v)\n\tcase int:\n\t\tc.AsInt().ToEqual(int64(v))\n\tcase int8:\n\t\tc.AsInt().ToEqual(int64(v))\n\tcase int16:\n\t\tc.AsInt().ToEqual(int64(v))\n\tcase int32:\n\t\tc.AsInt().ToEqual(int64(v))\n\tcase int64:\n\t\tc.AsInt().ToEqual(v)\n\tcase error:\n\t\tc.ctx.Fail(\"Error is not equal to value\\nGot: %s\", v.Error())\n\tdefault:\n\t\tc.ctx.Fail(\"Unsupported type: %T\", value)\n\t}\n}\n\n// ToContainErrorString asserts that current error value contains an error string.\nfunc (c ValueChecker) ToContainErrorString(msg string) {\n\tc.ctx.T().Helper()\n\n\terr, ok := c.value.(error)\n\tif !ok {\n\t\tc.ctx.Fail(\"Failed: expected an error value\\nGot: %T\", c.value)\n\t\treturn\n\t}\n\n\tc.ctx.CheckExpectation(strings.Contains(err.Error(), msg), func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn ufmt.Sprintf(\"Expected error message to contain: %s\\nGot: %s\", msg, err.Error())\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected error message not to contain: %s\\nGot: %s\", msg, err.Error())\n\t})\n}\n\n// AsString returns a checker to assert current value as a string.\nfunc (c ValueChecker) AsString() StringChecker {\n\tc.ctx.T().Helper()\n\n\tv, err := asString(c.value)\n\tif err != nil {\n\t\tc.ctx.Fail(\"Failed: %s: expected a string value\\nGot: %T\", err.Error(), c.value)\n\t\treturn StringChecker{}\n\t}\n\n\treturn NewStringChecker(c.ctx, v)\n}\n\n// AsBoolean returns a checker to assert current value as a boolean.\nfunc (c ValueChecker) AsBoolean() BooleanChecker {\n\tc.ctx.T().Helper()\n\n\tv, err := asBoolean(c.value)\n\tif err != nil {\n\t\tc.ctx.Fail(\"Failed: %s: expected a boolean value\\nGot: %T\", err.Error(), c.value)\n\t\treturn BooleanChecker{}\n\t}\n\n\treturn NewBooleanChecker(c.ctx, v)\n}\n\n// AsFloat returns a checker to assert current value as a float64.\nfunc (c ValueChecker) AsFloat() FloatChecker {\n\tc.ctx.T().Helper()\n\n\tv, err := asFloat(c.value)\n\tif err != nil {\n\t\tc.ctx.Fail(\"%s: expected a float value\\nGot: %T\", err.Error(), c.value)\n\t\treturn FloatChecker{}\n\t}\n\n\treturn NewFloatChecker(c.ctx, v)\n}\n\n// AsUint returns a checker to assert current value as a uint64.\nfunc (c ValueChecker) AsUint() UintChecker {\n\tc.ctx.T().Helper()\n\n\tv, err := asUint(c.value)\n\tif err != nil {\n\t\tc.ctx.Fail(\"Failed: %s: expected a uint value\\nGot: %T\", err.Error(), c.value)\n\t\treturn UintChecker{}\n\t}\n\n\treturn NewUintChecker(c.ctx, v)\n}\n\n// AsInt returns a checker to assert current value as a int64.\nfunc (c ValueChecker) AsInt() IntChecker {\n\tc.ctx.T().Helper()\n\n\tv, err := asInt(c.value)\n\tif err != nil {\n\t\tc.ctx.Fail(\"Failed: %s: expected an int value\\nGot: %T\", err.Error(), c.value)\n\t\treturn IntChecker{}\n\t}\n\n\treturn NewIntChecker(c.ctx, v)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ulist","path":"gno.land/p/moul/ulist","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/ulist\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"ulist.gno","body":"// Package ulist provides an append-only list implementation using a binary tree structure,\n// optimized for scenarios requiring sequential inserts with auto-incrementing indices.\n//\n// The implementation uses a binary tree where new elements are added by following a path\n// determined by the binary representation of the index. This provides automatic balancing\n// for append operations without requiring any balancing logic.\n//\n// Unlike the AVL tree-based list implementation (p/demo/avl/list), ulist is specifically\n// designed for append-only operations and does not require rebalancing. This makes it more\n// efficient for sequential inserts but less flexible for general-purpose list operations.\n//\n// Key differences from AVL list:\n// * Append-only design (no arbitrary inserts)\n// * No tree rebalancing needed\n// * Simpler implementation\n// * More memory efficient for sequential operations\n// * Less flexible than AVL (no arbitrary inserts/reordering)\n//\n// Key characteristics:\n// * O(log n) append and access operations\n// * Perfect balance for power-of-2 sizes\n// * No balancing needed\n// * Memory efficient\n// * Natural support for range queries\n// * Support for soft deletion of elements\n// * Forward and reverse iteration capabilities\n// * Offset-based iteration with count control\npackage ulist\n\n// TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones).\n// TODO: Use this ulist in moul/collection for the primary index.\n// TODO: Consider adding a \"compact\" method that removes nil nodes.\n// TODO: Benchmarks.\n\nimport (\n\t\"errors\"\n)\n\n// List represents an append-only binary tree list\ntype List struct {\n\troot       *treeNode\n\ttotalSize  int\n\tactiveSize int\n}\n\n// Entry represents a key-value pair in the list, where Index is the position\n// and Value is the stored data\ntype Entry struct {\n\tIndex int\n\tValue any\n}\n\n// treeNode represents a node in the binary tree\ntype treeNode struct {\n\tdata  any\n\tleft  *treeNode\n\tright *treeNode\n}\n\n// Error variables\nvar (\n\tErrOutOfBounds = errors.New(\"index out of bounds\")\n\tErrDeleted     = errors.New(\"element already deleted\")\n)\n\n// New creates a new empty List instance\nfunc New() *List {\n\treturn \u0026List{}\n}\n\n// Append adds one or more values to the end of the list.\n// Values are added sequentially, and the list grows automatically.\nfunc (l *List) Append(values ...any) {\n\tfor _, value := range values {\n\t\tindex := l.totalSize\n\t\tnode := l.findNode(index, true)\n\t\tnode.data = value\n\t\tl.totalSize++\n\t\tl.activeSize++\n\t}\n}\n\n// Get retrieves the value at the specified index.\n// Returns nil if the index is out of bounds or if the element was deleted.\nfunc (l *List) Get(index int) any {\n\tnode := l.findNode(index, false)\n\tif node == nil {\n\t\treturn nil\n\t}\n\treturn node.data\n}\n\n// Delete marks the elements at the specified indices as deleted.\n// Returns ErrOutOfBounds if any index is invalid or ErrDeleted if\n// the element was already deleted.\nfunc (l *List) Delete(indices ...int) error {\n\tif len(indices) == 0 {\n\t\treturn nil\n\t}\n\tif l == nil || l.totalSize == 0 {\n\t\treturn ErrOutOfBounds\n\t}\n\n\tfor _, index := range indices {\n\t\tif index \u003c 0 || index \u003e= l.totalSize {\n\t\t\treturn ErrOutOfBounds\n\t\t}\n\n\t\tnode := l.findNode(index, false)\n\t\tif node == nil || node.data == nil {\n\t\t\treturn ErrDeleted\n\t\t}\n\t\tnode.data = nil\n\t\tl.activeSize--\n\t}\n\n\treturn nil\n}\n\n// Set updates or restores a value at the specified index if within bounds\n// Returns ErrOutOfBounds if the index is invalid\nfunc (l *List) Set(index int, value any) error {\n\tif l == nil || index \u003c 0 || index \u003e= l.totalSize {\n\t\treturn ErrOutOfBounds\n\t}\n\n\tnode := l.findNode(index, false)\n\tif node == nil {\n\t\treturn ErrOutOfBounds\n\t}\n\n\t// If this is restoring a deleted element\n\tif value != nil \u0026\u0026 node.data == nil {\n\t\tl.activeSize++\n\t}\n\n\t// If this is deleting an element\n\tif value == nil \u0026\u0026 node.data != nil {\n\t\tl.activeSize--\n\t}\n\n\tnode.data = value\n\treturn nil\n}\n\n// Size returns the number of active (non-deleted) elements in the list\nfunc (l *List) Size() int {\n\tif l == nil {\n\t\treturn 0\n\t}\n\treturn l.activeSize\n}\n\n// TotalSize returns the total number of elements ever added to the list,\n// including deleted elements\nfunc (l *List) TotalSize() int {\n\tif l == nil {\n\t\treturn 0\n\t}\n\treturn l.totalSize\n}\n\n// IterCbFn is a callback function type used in iteration methods.\n// Return true to stop iteration, false to continue.\ntype IterCbFn func(index int, value any) bool\n\n// Iterator performs iteration between start and end indices, calling cb for each entry.\n// If start \u003e end, iteration is performed in reverse order.\n// Returns true if iteration was stopped early by the callback returning true.\n// Skips deleted elements.\nfunc (l *List) Iterator(start, end int, cb IterCbFn) bool {\n\t// For empty list or invalid range\n\tif l == nil || l.totalSize == 0 {\n\t\treturn false\n\t}\n\tif start \u003c 0 \u0026\u0026 end \u003c 0 {\n\t\treturn false\n\t}\n\tif start \u003e= l.totalSize \u0026\u0026 end \u003e= l.totalSize {\n\t\treturn false\n\t}\n\n\t// Normalize indices\n\tif start \u003c 0 {\n\t\tstart = 0\n\t}\n\tif end \u003c 0 {\n\t\tend = 0\n\t}\n\tif end \u003e= l.totalSize {\n\t\tend = l.totalSize - 1\n\t}\n\tif start \u003e= l.totalSize {\n\t\tstart = l.totalSize - 1\n\t}\n\n\t// Handle reverse iteration\n\tif start \u003e end {\n\t\tfor i := start; i \u003e= end; i-- {\n\t\t\tval := l.Get(i)\n\t\t\tif val != nil {\n\t\t\t\tif cb(i, val) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\t// Handle forward iteration\n\tfor i := start; i \u003c= end; i++ {\n\t\tval := l.Get(i)\n\t\tif val != nil {\n\t\t\tif cb(i, val) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// IteratorByOffset performs iteration starting from offset for count elements.\n// If count is positive, iterates forward; if negative, iterates backward.\n// The iteration stops after abs(count) elements or when reaching list bounds.\n// Skips deleted elements.\nfunc (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool {\n\tif count == 0 || l == nil || l.totalSize == 0 {\n\t\treturn false\n\t}\n\n\t// Normalize offset\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\tif offset \u003e= l.totalSize {\n\t\toffset = l.totalSize - 1\n\t}\n\n\t// Determine end based on count direction\n\tvar end int\n\tif count \u003e 0 {\n\t\tend = l.totalSize - 1\n\t} else {\n\t\tend = 0\n\t}\n\n\twrapperReturned := false\n\n\t// Wrap the callback to limit iterations\n\tremaining := abs(count)\n\twrapper := func(index int, value any) bool {\n\t\tif remaining \u003c= 0 {\n\t\t\twrapperReturned = true\n\t\t\treturn true\n\t\t}\n\t\tremaining--\n\t\treturn cb(index, value)\n\t}\n\tret := l.Iterator(offset, end, wrapper)\n\tif wrapperReturned {\n\t\treturn false\n\t}\n\treturn ret\n}\n\n// abs returns the absolute value of x\nfunc abs(x int) int {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\n// findNode locates or creates a node at the given index in the binary tree.\n// The tree is structured such that the path to a node is determined by the binary\n// representation of the index. For example, a tree with 15 elements would look like:\n//\n//\t          0\n//\t       /      \\\n//\t     1         2\n//\t   /   \\     /   \\\n//\t  3    4    5     6\n//\t / \\  / \\  / \\   / \\\n//\t7  8 9 10 11 12 13 14\n//\n// To find index 13 (binary 1101):\n// 1. Start at root (0)\n// 2. Calculate bits needed (4 bits for index 13)\n// 3. Skip the highest bit position and start from bits-2\n// 4. Read bits from left to right:\n//   - 1 -\u003e go right to 2\n//   - 1 -\u003e go right to 6\n//   - 0 -\u003e go left to 13\n//\n// Special cases:\n// - Index 0 always returns the root node\n// - For create=true, missing nodes are created along the path\n// - For create=false, returns nil if any node is missing\nfunc (l *List) findNode(index int, create bool) *treeNode {\n\t// For read operations, check bounds strictly\n\tif !create \u0026\u0026 (l == nil || index \u003c 0 || index \u003e= l.totalSize) {\n\t\treturn nil\n\t}\n\n\t// For create operations, allow index == totalSize for append\n\tif create \u0026\u0026 (l == nil || index \u003c 0 || index \u003e l.totalSize) {\n\t\treturn nil\n\t}\n\n\t// Initialize root if needed\n\tif l.root == nil {\n\t\tif !create {\n\t\t\treturn nil\n\t\t}\n\t\tl.root = \u0026treeNode{}\n\t\treturn l.root\n\t}\n\n\tnode := l.root\n\n\t// Special case for root node\n\tif index == 0 {\n\t\treturn node\n\t}\n\n\t// Calculate the number of bits needed (inline highestBit logic)\n\tbits := 0\n\tn := index + 1\n\tfor n \u003e 0 {\n\t\tn \u003e\u003e= 1\n\t\tbits++\n\t}\n\n\t// Start from the second highest bit\n\tfor level := bits - 2; level \u003e= 0; level-- {\n\t\tbit := (index \u0026 (1 \u003c\u003c uint(level))) != 0\n\n\t\tif bit {\n\t\t\tif node.right == nil {\n\t\t\t\tif !create {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tnode.right = \u0026treeNode{}\n\t\t\t}\n\t\t\tnode = node.right\n\t\t} else {\n\t\t\tif node.left == nil {\n\t\t\t\tif !create {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tnode.left = \u0026treeNode{}\n\t\t\t}\n\t\t\tnode = node.left\n\t\t}\n\t}\n\n\treturn node\n}\n\n// MustDelete deletes elements at the specified indices.\n// Panics if any index is invalid or if any element was already deleted.\nfunc (l *List) MustDelete(indices ...int) {\n\tif err := l.Delete(indices...); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// MustGet retrieves the value at the specified index.\n// Panics if the index is out of bounds or if the element was deleted.\nfunc (l *List) MustGet(index int) any {\n\tif l == nil || index \u003c 0 || index \u003e= l.totalSize {\n\t\tpanic(ErrOutOfBounds)\n\t}\n\tvalue := l.Get(index)\n\tif value == nil {\n\t\tpanic(ErrDeleted)\n\t}\n\treturn value\n}\n\n// MustSet updates or restores a value at the specified index.\n// Panics if the index is out of bounds.\nfunc (l *List) MustSet(index int, value any) {\n\tif err := l.Set(index, value); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetRange returns a slice of Entry containing elements between start and end indices.\n// If start \u003e end, elements are returned in reverse order.\n// Deleted elements are skipped.\nfunc (l *List) GetRange(start, end int) []Entry {\n\tvar entries []Entry\n\tl.Iterator(start, end, func(index int, value any) bool {\n\t\tentries = append(entries, Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// GetByOffset returns a slice of Entry starting from offset for count elements.\n// If count is positive, returns elements forward; if negative, returns elements backward.\n// The operation stops after abs(count) elements or when reaching list bounds.\n// Deleted elements are skipped.\nfunc (l *List) GetByOffset(offset int, count int) []Entry {\n\tvar entries []Entry\n\tl.IteratorByOffset(offset, count, func(index int, value any) bool {\n\t\tentries = append(entries, Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// IList defines the interface for an ulist.List compatible structure.\ntype IList interface {\n\t// Basic operations\n\tAppend(values ...any)\n\tGet(index int) any\n\tDelete(indices ...int) error\n\tSize() int\n\tTotalSize() int\n\tSet(index int, value any) error\n\n\t// Must variants that panic instead of returning errors\n\tMustDelete(indices ...int)\n\tMustGet(index int) any\n\tMustSet(index int, value any)\n\n\t// Range operations\n\tGetRange(start, end int) []Entry\n\tGetByOffset(offset int, count int) []Entry\n\n\t// Iterator operations\n\tIterator(start, end int, cb IterCbFn) bool\n\tIteratorByOffset(offset int, count int, cb IterCbFn) bool\n}\n\n// Verify that List implements IList\nvar _ IList = (*List)(nil)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"message","path":"gno.land/p/jeronimoalbi/message","files":[{"name":"broker.gno","body":"package message\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/ulist\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar (\n\t// ErrInvalidTopic is triggered when an invalid topic is used.\n\tErrInvalidTopic = errors.New(\"invalid topic\")\n\n\t// ErrRequiredCallback is triggered when subscribing without a callback.\n\tErrRequiredCallback = errors.New(\"message callback is required\")\n\n\t// ErrRequiredSubscriptionID is triggered when unsubscribing without an ID.\n\tErrRequiredSubscriptionID = errors.New(\"message sibscription ID is required\")\n\n\t// ErrRequiredTopic is triggered when (un)subscribing without a topic.\n\tErrRequiredTopic = errors.New(\"message topic is required\")\n)\n\n// NewBroker creates a new message broker.\nfunc NewBroker() *Broker {\n\treturn \u0026Broker{}\n}\n\n// Broker is a message broker that handles subscriptions and message publishing.\ntype Broker struct {\n\tcallbacks avl.Tree // string(topic) -\u003e *ulist.List(Callback)\n}\n\n// Topics returns the list of current subscription topics.\nfunc (b Broker) Topics() []Topic {\n\tvar topics []Topic\n\tb.callbacks.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\ttopic := Topic(k)\n\t\tif topic == TopicAll {\n\t\t\t// Skip catchall topic from the list\n\t\t\treturn false\n\t\t}\n\n\t\ttopics = append(topics, topic)\n\t\treturn false\n\t})\n\treturn topics\n}\n\n// Subscribe subscribes to messages published for a topic.\n// It returns the callback ID within the topic.\nfunc (b *Broker) Subscribe(topic Topic, cb Callback) (id int, _ error) {\n\tkey := strings.TrimSpace(string(topic))\n\tif key == \"\" {\n\t\treturn 0, ErrRequiredTopic\n\t}\n\n\tif cb == nil {\n\t\treturn 0, ErrRequiredCallback\n\t}\n\n\tv := b.callbacks.Get(key)\n\tcallbacks, _ := v.(*ulist.List)\n\tif callbacks == nil {\n\t\tcallbacks = ulist.New()\n\t}\n\n\tcallbacks.Append(cb)\n\tb.callbacks.Set(key, callbacks)\n\treturn callbacks.TotalSize(), nil\n}\n\n// Unsubscribe unsubscribes a callback from a message topic.\n// ID is the callback ID within the topic, returned on subscription.\nfunc (b *Broker) Unsubscribe(topic Topic, id int) (unsubscribed bool, _ error) {\n\tkey := strings.TrimSpace(string(topic))\n\tif key == \"\" {\n\t\treturn false, ErrRequiredTopic\n\t}\n\n\tif id == 0 {\n\t\treturn false, ErrRequiredSubscriptionID\n\t}\n\n\tv := b.callbacks.Get(key)\n\tif v == nil {\n\t\treturn false, errors.New(\"message topic not found: \" + key)\n\t}\n\n\tcallbacks := v.(*ulist.List)\n\ti := id - 1\n\treturn callbacks.Delete(i) == nil, nil\n}\n\n// Publish publishes a message for a topic.\nfunc (b Broker) Publish(topic Topic, data any) error {\n\tif topic == TopicAll {\n\t\treturn ErrInvalidTopic\n\t}\n\n\tkey := strings.TrimSpace(string(topic))\n\tif key == \"\" {\n\t\treturn ErrRequiredTopic\n\t}\n\n\titerCb := func(_ int, v any) bool {\n\t\tcb := v.(Callback)\n\t\tcb(Message{topic, data})\n\t\treturn false\n\t}\n\n\t// Trigger callbacks subscribed to current topic\n\tv := b.callbacks.Get(key)\n\tif v != nil {\n\t\tcallbacks := v.(*ulist.List)\n\t\tcallbacks.Iterator(0, callbacks.Size(), iterCb)\n\t}\n\n\t// Trigger callbacks subscribed to all topics\n\tv = b.callbacks.Get(string(TopicAll))\n\tif v != nil {\n\t\tcallbacks := v.(*ulist.List)\n\t\tcallbacks.Iterator(0, callbacks.Size(), iterCb)\n\t}\n\treturn nil\n}\n"},{"name":"doc.gno","body":"// Package message provides a simple message broker implementation.\n//\n// The message broker is a Pub/Sub one. It implements two different interfaces,\n// `Publisher` and `Subscriber`, which are also defined within this package.\n//\n// Published messages contain the topic where they are published and optional\n// message data.\n//\n// Subscribe to an event:\n//\n//\tbroker := message.NewBroker()\n//\tsubID, err := broker.Subscribe(\"EventName\", func(msg message.Message) {\n//\t   println(\"EventName has been triggered\")\n//\t   println(msg.Data)\n//\t})\n//\tif err != nil {\n//\t   panic(err)\n//\t}\n//\n// Unsubscribe from an event:\n//\n//\tunsubscribed, err := broker.Unsubscribe(\"EventName\", subID)\n//\tif err != nil {\n//\t   panic(err)\n//\t}\n//\n//\tif !unsubscribed {\n//\t   panic(\"subscription not found\")\n//\t}\n//\n// Publish an event:\n//\n//\terr := broker.Publish(\"EventName\", \"Example event data\")\n//\tif err != nil {\n//\t   panic(err)\n//\t}\npackage message\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/message\"\ngno = \"0.9\"\n"},{"name":"message.gno","body":"package message\n\n// TopicAll defines a topic for all types of message.\n// This topic can be used to subscribe to message for all topics.\nconst TopicAll Topic = \"*\"\n\ntype (\n\t// Topic defines a type for message topics.\n\tTopic string\n\n\t// Callback defines a type for message callbacks.\n\tCallback func(Message)\n\n\t// Message defines a type for published messages.\n\tMessage struct {\n\t\t// Topic is the message topic.\n\t\tTopic Topic\n\n\t\t// Data contains optional message data.\n\t\tData any\n\t}\n\n\t// Publisher defines an interface for message publishers.\n\tPublisher interface {\n\t\t// Publish publishes a message for a topic.\n\t\tPublish(_ Topic, data any) error\n\t}\n\n\t// Subscriber defines an interface for message subscribers.\n\tSubscriber interface {\n\t\t// Subscribe subscribes to messages published for a topic.\n\t\t// It returns the callback ID within the topic.\n\t\tSubscribe(Topic, Callback) (id int, _ error)\n\n\t\t// Unsubscribe unsubscribes a callback from a message topic.\n\t\t// ID is the callback ID within the topic, returned on subscription.\n\t\tUnsubscribe(_ Topic, id int) (unsubscribed bool, _ error)\n\t}\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"echo","path":"gno.land/r/archive/echo","files":[{"name":"echo.gno","body":"package echo\n\n/*\n * This realm echoes the `path` argument it received.\n * Can be used by developers as a simple endpoint to test\n * forbidden characters, for pentesting or simply to\n * test it works.\n *\n * See also r/demo/print (to print various thing like user address)\n */\nfunc Render(path string) string {\n\treturn path\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/archive/echo\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"mdform","path":"gno.land/p/jeronimoalbi/mdform","files":[{"name":"README.md","body":"# Markdown Form Package\n\nThe package provides a very simplistic [Gno-Flavored Markdown form](/r/docs/markdown#forms) generator.\n\nForms can be created by sequentially calling form methods to create each one of the form fields.\n\nExample usage:\n\n```go\nimport \"gno.land/p/jeronimoalbi/mdform\"\n\nfunc Render(string) string {\n    form := mdform.New()\n\n    // Add a text input field\n    form.Input(\n        \"name\",\n        \"placeholder\", \"Name\",\n        \"value\", \"John Doe\",\n    )\n\n    // Add a select field with three possible values\n    form.Select(\n        \"country\",\n        \"United States\",\n        \"description\", \"Select your country\",\n    )\n    form.Select(\n        \"country\",\n        \"Spain\",\n    )\n    form.Select(\n        \"country\",\n        \"Germany\",\n    )\n\n    // Add a checkbox group with two possible values\n    form.Checkbox(\n        \"interests\",\n        \"music\",\n        \"description\", \"What do you like to do?\",\n    )\n    form.Checkbox(\n        \"interests\",\n        \"tech\",\n        \"checked\", \"true\",\n    )\n\n    return form.String()\n}\n```\n\nForm output:\n\n```html\n\u003cgno-form exec=\"FunctionName\"\u003e\n    \u003cgno-input name=\"name\" placeholder=\"Name\" value=\"John Doe\" /\u003e\n    \u003cgno-select name=\"country\" value=\"United States\" description=\"Select your country\" /\u003e\n    \u003cgno-select name=\"country\" value=\"Spain\" /\u003e\n    \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n    \u003cgno-input type=\"checkbox\" name=\"interests\" value=\"music\" description=\"What do you like to do?\" /\u003e\n    \u003cgno-input type=\"checkbox\" name=\"interests\" value=\"tech\" checked=\"true\" /\u003e\n\u003c/gno-form\u003e\n```\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/mdform\"\ngno = \"0.9\"\n"},{"name":"mdform.gno","body":"package mdform\n\nimport (\n\t\"html\"\n\t\"strings\"\n)\n\nconst (\n\tInputTypeText     = \"text\"\n\tInputTypeNumber   = \"number\"\n\tInputTypeEmail    = \"email\"\n\tInputTypePhone    = \"tel\"\n\tInputTypePassword = \"password\"\n\tInputTypeRadio    = \"radio\"\n\tInputTypeCheckbox = \"checkbox\"\n)\n\nvar (\n\tformAttributes     = []string{\"exec\", \"path\"}\n\tinputAttributes    = []string{\"checked\", \"description\", \"placeholder\", \"readonly\", \"required\", \"type\", \"value\"}\n\ttextareaAttributes = []string{\"placeholder\", \"readonly\", \"required\", \"rows\", \"value\"}\n\tselectAttributes   = []string{\"description\", \"readonly\", \"required\", \"selected\"}\n)\n\n// New creates a new form.\nfunc New(attributes ...string) *Form {\n\tassertEvenAttributes(attributes)\n\n\tform := \u0026Form{}\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\n\t\tassertIsValidAttribute(name, formAttributes)\n\n\t\tform.attrs = append(form.attrs, formatAttribute(name, value))\n\t}\n\treturn form\n}\n\n// Form is a form that can be rendered to Gno-Flavored Markdown.\ntype Form struct {\n\tattrs  []string\n\tfields []string\n}\n\n// Input appends a new input to form fields.\n// Use `Form.Radio()` or `Form.Checkbox()` to append those types of inputs to the form.\n// Method panics when appending inputs of type radio or checkbox, or when attributes are not valid.\nfunc (f *Form) Input(name string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form input name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{formatAttribute(\"name\", name)}\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\t\tif name == \"type\" {\n\t\t\tswitch value {\n\t\t\tcase InputTypeRadio:\n\t\t\t\tpanic(\"use form.Radio() to create inputs of type radio\")\n\t\t\tcase InputTypeCheckbox:\n\t\t\t\tpanic(\"use form.Checkbox() to create inputs of type checkbox\")\n\t\t\t}\n\t\t}\n\n\t\tassertIsValidAttribute(name, inputAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-input \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\n// Radio appends a new input of type radio to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Radio(name, value string, attributes ...string) *Form {\n\treturn f.appendInputType(InputTypeRadio, name, value, attributes...)\n}\n\n// Checkbox appends a new input of type checkbox to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Checkbox(name, value string, attributes ...string) *Form {\n\treturn f.appendInputType(InputTypeCheckbox, name, value, attributes...)\n}\n\n// Textarea appends a new textarea to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Textarea(name string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form textarea name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{formatAttribute(\"name\", name)}\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\n\t\tassertIsValidAttribute(name, textareaAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-textarea \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\n// Select appends a new select to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Select(name, value string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form select name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{\n\t\tformatAttribute(\"name\", name),\n\t\tformatAttribute(\"value\", value),\n\t}\n\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\n\t\tassertIsValidAttribute(name, selectAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-select \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\n// String returns the form as Gno-Flavored Markdown.\nfunc (f Form) String() string {\n\tfields := strings.Join(f.fields, \"\\n\")\n\tattrs := strings.Join(f.attrs, \" \")\n\tif len(attrs) \u003e 0 {\n\t\tattrs = \" \" + attrs\n\t}\n\n\treturn \"\u003cgno-form\" + attrs + \"\u003e\\n\" + fields + \"\\n\u003c/gno-form\u003e\\n\"\n}\n\nfunc (f *Form) appendInputType(typeName, name, value string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form \" + typeName + \" input name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{\n\t\tformatAttribute(\"type\", typeName),\n\t\tformatAttribute(\"name\", name),\n\t\tformatAttribute(\"value\", value),\n\t}\n\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\t\tif name == \"type\" || name == \"value\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tassertIsValidAttribute(name, inputAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-input \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\nfunc formatAttribute(name, value string) string {\n\treturn name + `=\"` + html.EscapeString(value) + `\"`\n}\n\nfunc assertEvenAttributes(attrs []string) {\n\tif len(attrs)%2 != 0 {\n\t\tpanic(\"expected an even number of attribute arguments\")\n\t}\n}\n\nfunc assertIsValidAttribute(attr string, attrs []string) {\n\tfor _, name := range attrs {\n\t\tif name == attr {\n\t\t\treturn\n\t\t}\n\t}\n\n\tpanic(\"invalid attribute: \" + attr)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"uint256","path":"gno.land/p/onbloc/uint256","files":[{"name":"LICENSE","body":"BSD 3-Clause License\n\nCopyright 2020 uint256 Authors\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"},{"name":"README.md","body":"# Fixed size 256-bit math library\n\nThis is a library specialized at replacing the `big.Int` library for math based on 256-bit types.\n\noriginal repository: [uint256](\u003chttps://github.com/holiman/uint256/tree/master\u003e)\n"},{"name":"arithmetic.gno","body":"// arithmetic provides arithmetic operations for Uint objects.\n// This includes basic binary operations such as addition, subtraction, multiplication, division, and modulo operations\n// as well as overflow checks, and negation. These functions are essential for numeric\n// calculations using 256-bit unsigned integers.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Add sets z to the sum x+y\nfunc (z *Uint) Add(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// AddOverflow sets z to the sum x+y, and returns z and whether overflow occurred\nfunc (z *Uint) AddOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Sub sets z to the difference x-y\nfunc (z *Uint) Sub(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// SubOverflow sets z to the difference x-y and returns z and true if the operation underflowed\nfunc (z *Uint) SubOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Neg returns -x mod 2^256.\nfunc (z *Uint) Neg(x *Uint) *Uint {\n\treturn z.Sub(new(Uint), x)\n}\n\n// commented out for possible overflow\n// Mul sets z to the product x*y\nfunc (z *Uint) Mul(x, y *Uint) *Uint {\n\tvar (\n\t\tres              Uint\n\t\tcarry            uint64\n\t\tres1, res2, res3 uint64\n\t)\n\n\tcarry, res.arr[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tres3 = x.arr[3]*y.arr[0] + carry\n\n\tcarry, res.arr[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tres3 = res3 + x.arr[2]*y.arr[1] + carry\n\n\tcarry, res.arr[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tres3 = res3 + x.arr[1]*y.arr[2] + carry\n\n\tres.arr[3] = res3 + x.arr[0]*y.arr[3]\n\n\treturn z.Set(\u0026res)\n}\n\n// MulOverflow sets z to the product x*y, and returns z and  whether overflow occurred\nfunc (z *Uint) MulOverflow(x, y *Uint) (*Uint, bool) {\n\tp := umul(x, y)\n\tcopy(z.arr[:], p[:4])\n\treturn z, (p[4] | p[5] | p[6] | p[7]) != 0\n}\n\n// commented out for possible overflow\n// Div sets z to the quotient x/y for returns z.\n// If y == 0, z is set to 0\nfunc (z *Uint) Div(x, y *Uint) *Uint {\n\tif y.IsZero() || y.Gt(x) {\n\t\treturn z.Clear()\n\t}\n\tif x.Eq(y) {\n\t\treturn z.SetOne()\n\t}\n\t// Shortcut some cases\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() / y.Uint64())\n\t}\n\n\t// At this point, we know\n\t// x/y ; x \u003e y \u003e 0\n\n\tvar quot Uint\n\tudivrem(quot.arr[:], x.arr[:], y)\n\treturn z.Set(\u0026quot)\n}\n\n// MulMod calculates the modulo-m multiplication of x and y and\n// returns z.\n// If m == 0, z is set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) MulMod(x, y, m *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() || m.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tp := umul(x, y)\n\n\tif m.arr[3] != 0 {\n\t\tmu := Reciprocal(m)\n\t\tr := reduce4(p, m, mu)\n\t\treturn z.Set(\u0026r)\n\t}\n\n\tvar (\n\t\tpl Uint\n\t\tph Uint\n\t)\n\n\tpl = Uint{arr: [4]uint64{p[0], p[1], p[2], p[3]}}\n\tph = Uint{arr: [4]uint64{p[4], p[5], p[6], p[7]}}\n\n\t// If the multiplication is within 256 bits use Mod().\n\tif ph.IsZero() {\n\t\treturn z.Mod(\u0026pl, m)\n\t}\n\n\tvar quot [8]uint64\n\trem := udivrem(quot[:], p[:], m)\n\treturn z.Set(\u0026rem)\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// If y == 0, z is set to 0 (OBS: differs from the big.Uint)\nfunc (z *Uint) Mod(x, y *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tswitch x.Cmp(y) {\n\tcase -1:\n\t\t// x \u003c y\n\t\tcopy(z.arr[:], x.arr[:])\n\t\treturn z\n\tcase 0:\n\t\t// x == y\n\t\treturn z.Clear() // They are equal\n\t}\n\n\t// At this point:\n\t// x != 0\n\t// y != 0\n\t// x \u003e y\n\n\t// Shortcut trivial case\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() % y.Uint64())\n\t}\n\n\tvar quot Uint\n\t*z = udivrem(quot.arr[:], x.arr[:], y)\n\treturn z\n}\n\n// DivMod sets z to the quotient x div y and m to the modulus x mod y and returns the pair (z, m) for y != 0.\n// If y == 0, both z and m are set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) DivMod(x, y, m *Uint) (*Uint, *Uint) {\n\tif y.IsZero() {\n\t\treturn z.Clear(), m.Clear()\n\t}\n\tvar quot Uint\n\t*m = udivrem(quot.arr[:], x.arr[:], y)\n\t*z = quot\n\treturn z, m\n}\n\n// Exp sets z = base**exponent mod 2**256, and returns z.\nfunc (z *Uint) Exp(base, exponent *Uint) *Uint {\n\tres := Uint{arr: [4]uint64{1, 0, 0, 0}}\n\tmultiplier := *base\n\texpBitLen := exponent.BitLen()\n\n\tcurBit := 0\n\tword := exponent.arr[0]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 64; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[1]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 128; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[2]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 192; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[3]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 256; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\treturn z.Set(\u0026res)\n}\n\nfunc (z *Uint) squared() {\n\tvar (\n\t\tres                    Uint\n\t\tcarry0, carry1, carry2 uint64\n\t\tres1, res2             uint64\n\t)\n\n\tcarry0, res.arr[0] = bits.Mul64(z.arr[0], z.arr[0])\n\tcarry0, res1 = umulHop(carry0, z.arr[0], z.arr[1])\n\tcarry0, res2 = umulHop(carry0, z.arr[0], z.arr[2])\n\n\tcarry1, res.arr[1] = umulHop(res1, z.arr[0], z.arr[1])\n\tcarry1, res2 = umulStep(res2, z.arr[1], z.arr[1], carry1)\n\n\tcarry2, res.arr[2] = umulHop(res2, z.arr[0], z.arr[2])\n\n\tres.arr[3] = 2*(z.arr[0]*z.arr[3]+z.arr[1]*z.arr[2]) + carry0 + carry1 + carry2\n\n\tz.Set(\u0026res)\n}\n\n// udivrem divides u by d and produces both quotient and remainder.\n// The quotient is stored in provided quot - len(u)-len(d)+1 words.\n// It loosely follows the Knuth's division algorithm (sometimes referenced as \"schoolbook\" division) using 64-bit words.\n// See Knuth, Volume 2, section 4.3.1, Algorithm D.\nfunc udivrem(quot, u []uint64, d *Uint) (rem Uint) {\n\tvar dLen int\n\tfor i := len(d.arr) - 1; i \u003e= 0; i-- {\n\t\tif d.arr[i] != 0 {\n\t\t\tdLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tshift := uint(bits.LeadingZeros64(d.arr[dLen-1]))\n\n\tvar dnStorage Uint\n\tdn := dnStorage.arr[:dLen]\n\tfor i := dLen - 1; i \u003e 0; i-- {\n\t\tdn[i] = (d.arr[i] \u003c\u003c shift) | (d.arr[i-1] \u003e\u003e (64 - shift))\n\t}\n\tdn[0] = d.arr[0] \u003c\u003c shift\n\n\tvar uLen int\n\tfor i := len(u) - 1; i \u003e= 0; i-- {\n\t\tif u[i] != 0 {\n\t\t\tuLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif uLen \u003c dLen {\n\t\tcopy(rem.arr[:], u)\n\t\treturn rem\n\t}\n\n\tvar unStorage [9]uint64\n\tun := unStorage[:uLen+1]\n\tun[uLen] = u[uLen-1] \u003e\u003e (64 - shift)\n\tfor i := uLen - 1; i \u003e 0; i-- {\n\t\tun[i] = (u[i] \u003c\u003c shift) | (u[i-1] \u003e\u003e (64 - shift))\n\t}\n\tun[0] = u[0] \u003c\u003c shift\n\n\t// TODO: Skip the highest word of numerator if not significant.\n\n\tif dLen == 1 {\n\t\tr := udivremBy1(quot, un, dn[0])\n\t\trem.SetUint64(r \u003e\u003e shift)\n\t\treturn rem\n\t}\n\n\tudivremKnuth(quot, un, dn)\n\n\tfor i := 0; i \u003c dLen-1; i++ {\n\t\trem.arr[i] = (un[i] \u003e\u003e shift) | (un[i+1] \u003c\u003c (64 - shift))\n\t}\n\trem.arr[dLen-1] = un[dLen-1] \u003e\u003e shift\n\n\treturn rem\n}\n\n// umul computes full 256 x 256 -\u003e 512 multiplication.\nfunc umul(x, y *Uint) [8]uint64 {\n\tvar (\n\t\tres                           [8]uint64\n\t\tcarry, carry4, carry5, carry6 uint64\n\t\tres1, res2, res3, res4, res5  uint64\n\t)\n\n\tcarry, res[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tcarry4, res3 = umulHop(carry, x.arr[3], y.arr[0])\n\n\tcarry, res[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tcarry, res3 = umulStep(res3, x.arr[2], y.arr[1], carry)\n\tcarry5, res4 = umulStep(carry4, x.arr[3], y.arr[1], carry)\n\n\tcarry, res[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tcarry, res3 = umulStep(res3, x.arr[1], y.arr[2], carry)\n\tcarry, res4 = umulStep(res4, x.arr[2], y.arr[2], carry)\n\tcarry6, res5 = umulStep(carry5, x.arr[3], y.arr[2], carry)\n\n\tcarry, res[3] = umulHop(res3, x.arr[0], y.arr[3])\n\tcarry, res[4] = umulStep(res4, x.arr[1], y.arr[3], carry)\n\tcarry, res[5] = umulStep(res5, x.arr[2], y.arr[3], carry)\n\tres[7], res[6] = umulStep(carry6, x.arr[3], y.arr[3], carry)\n\n\treturn res\n}\n\n// umulStep computes (hi * 2^64 + lo) = z + (x * y) + carry.\nfunc umulStep(z, x, y, carry uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry = bits.Add64(lo, carry, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\tlo, carry = bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// umulHop computes (hi * 2^64 + lo) = z + (x * y)\nfunc umulHop(z, x, y uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry := bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// udivremBy1 divides u by single normalized word d and produces both quotient and remainder.\n// The quotient is stored in provided quot.\nfunc udivremBy1(quot, u []uint64, d uint64) (rem uint64) {\n\treciprocal := reciprocal2by1(d)\n\trem = u[len(u)-1] // Set the top word as remainder.\n\tfor j := len(u) - 2; j \u003e= 0; j-- {\n\t\tquot[j], rem = udivrem2by1(rem, u[j], d, reciprocal)\n\t}\n\treturn rem\n}\n\n// udivremKnuth implements the division of u by normalized multiple word d from the Knuth's division algorithm.\n// The quotient is stored in provided quot - len(u)-len(d) words.\n// Updates u to contain the remainder - len(d) words.\nfunc udivremKnuth(quot, u, d []uint64) {\n\tdh := d[len(d)-1]\n\tdl := d[len(d)-2]\n\treciprocal := reciprocal2by1(dh)\n\n\tfor j := len(u) - len(d) - 1; j \u003e= 0; j-- {\n\t\tu2 := u[j+len(d)]\n\t\tu1 := u[j+len(d)-1]\n\t\tu0 := u[j+len(d)-2]\n\n\t\tvar qhat, rhat uint64\n\t\tif u2 \u003e= dh { // Division overflows.\n\t\t\tqhat = ^uint64(0)\n\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t} else {\n\t\t\tqhat, rhat = udivrem2by1(u2, u1, dh, reciprocal)\n\t\t\tph, pl := bits.Mul64(qhat, dl)\n\t\t\tif ph \u003e rhat || (ph == rhat \u0026\u0026 pl \u003e u0) {\n\t\t\t\tqhat--\n\t\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t\t}\n\t\t}\n\n\t\t// Multiply and subtract.\n\t\tborrow := subMulTo(u[j:], d, qhat)\n\t\tu[j+len(d)] = u2 - borrow\n\t\tif u2 \u003c borrow { // Too much subtracted, add back.\n\t\t\tqhat--\n\t\t\tu[j+len(d)] += addTo(u[j:], d)\n\t\t}\n\n\t\tquot[j] = qhat // Store quotient digit.\n\t}\n}\n\n// isBitSet returns true if bit n-th is set, where n = 0 is LSB.\n// The n must be \u003c= 255.\nfunc (z *Uint) isBitSet(n uint) bool {\n\treturn (z.arr[n/64] \u0026 (1 \u003c\u003c (n % 64))) != 0\n}\n\n// addTo computes x += y.\n// Requires len(x) \u003e= len(y).\nfunc addTo(x, y []uint64) uint64 {\n\tvar carry uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\tx[i], carry = bits.Add64(x[i], y[i], carry)\n\t}\n\treturn carry\n}\n\n// subMulTo computes x -= y * multiplier.\n// Requires len(x) \u003e= len(y).\nfunc subMulTo(x, y []uint64, multiplier uint64) uint64 {\n\tvar borrow uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\ts, carry1 := bits.Sub64(x[i], borrow, 0)\n\t\tph, pl := bits.Mul64(y[i], multiplier)\n\t\tt, carry2 := bits.Sub64(s, pl, 0)\n\t\tx[i] = t\n\t\tborrow = ph + carry1 + carry2\n\t}\n\treturn borrow\n}\n\n// reciprocal2by1 computes \u003c^d, ^0\u003e / d.\nfunc reciprocal2by1(d uint64) uint64 {\n\treciprocal, _ := bits.Div64(^d, ^uint64(0), d)\n\treturn reciprocal\n}\n\n// udivrem2by1 divides \u003cuh, ul\u003e / d and produces both quotient and remainder.\n// It uses the provided d's reciprocal.\n// Implementation ported from https://github.com/chfast/intx and is based on\n// \"Improved division by invariant integers\", Algorithm 4.\nfunc udivrem2by1(uh, ul, d, reciprocal uint64) (quot, rem uint64) {\n\tqh, ql := bits.Mul64(reciprocal, uh)\n\tql, carry := bits.Add64(ql, ul, 0)\n\tqh, _ = bits.Add64(qh, uh, carry)\n\tqh++\n\n\tr := ul - qh*d\n\n\tif r \u003e ql {\n\t\tqh--\n\t\tr += d\n\t}\n\n\tif r \u003e= d {\n\t\tqh++\n\t\tr -= d\n\t}\n\n\treturn qh, r\n}\n"},{"name":"bits_table.gno","body":"// Copyright 2017 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Code generated by go run make_tables.go. DO NOT EDIT.\n\npackage uint256\n\nconst ntz8tab = \"\" +\n\t\"\\x08\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x07\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\"\n\nconst pop8tab = \"\" +\n\t\"\\x00\\x01\\x01\\x02\\x01\\x02\\x02\\x03\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\\x05\\x06\\x06\\x07\\x06\\x07\\x07\\x08\"\n\nconst rev8tab = \"\" +\n\t\"\\x00\\x80\\x40\\xc0\\x20\\xa0\\x60\\xe0\\x10\\x90\\x50\\xd0\\x30\\xb0\\x70\\xf0\" +\n\t\"\\x08\\x88\\x48\\xc8\\x28\\xa8\\x68\\xe8\\x18\\x98\\x58\\xd8\\x38\\xb8\\x78\\xf8\" +\n\t\"\\x04\\x84\\x44\\xc4\\x24\\xa4\\x64\\xe4\\x14\\x94\\x54\\xd4\\x34\\xb4\\x74\\xf4\" +\n\t\"\\x0c\\x8c\\x4c\\xcc\\x2c\\xac\\x6c\\xec\\x1c\\x9c\\x5c\\xdc\\x3c\\xbc\\x7c\\xfc\" +\n\t\"\\x02\\x82\\x42\\xc2\\x22\\xa2\\x62\\xe2\\x12\\x92\\x52\\xd2\\x32\\xb2\\x72\\xf2\" +\n\t\"\\x0a\\x8a\\x4a\\xca\\x2a\\xaa\\x6a\\xea\\x1a\\x9a\\x5a\\xda\\x3a\\xba\\x7a\\xfa\" +\n\t\"\\x06\\x86\\x46\\xc6\\x26\\xa6\\x66\\xe6\\x16\\x96\\x56\\xd6\\x36\\xb6\\x76\\xf6\" +\n\t\"\\x0e\\x8e\\x4e\\xce\\x2e\\xae\\x6e\\xee\\x1e\\x9e\\x5e\\xde\\x3e\\xbe\\x7e\\xfe\" +\n\t\"\\x01\\x81\\x41\\xc1\\x21\\xa1\\x61\\xe1\\x11\\x91\\x51\\xd1\\x31\\xb1\\x71\\xf1\" +\n\t\"\\x09\\x89\\x49\\xc9\\x29\\xa9\\x69\\xe9\\x19\\x99\\x59\\xd9\\x39\\xb9\\x79\\xf9\" +\n\t\"\\x05\\x85\\x45\\xc5\\x25\\xa5\\x65\\xe5\\x15\\x95\\x55\\xd5\\x35\\xb5\\x75\\xf5\" +\n\t\"\\x0d\\x8d\\x4d\\xcd\\x2d\\xad\\x6d\\xed\\x1d\\x9d\\x5d\\xdd\\x3d\\xbd\\x7d\\xfd\" +\n\t\"\\x03\\x83\\x43\\xc3\\x23\\xa3\\x63\\xe3\\x13\\x93\\x53\\xd3\\x33\\xb3\\x73\\xf3\" +\n\t\"\\x0b\\x8b\\x4b\\xcb\\x2b\\xab\\x6b\\xeb\\x1b\\x9b\\x5b\\xdb\\x3b\\xbb\\x7b\\xfb\" +\n\t\"\\x07\\x87\\x47\\xc7\\x27\\xa7\\x67\\xe7\\x17\\x97\\x57\\xd7\\x37\\xb7\\x77\\xf7\" +\n\t\"\\x0f\\x8f\\x4f\\xcf\\x2f\\xaf\\x6f\\xef\\x1f\\x9f\\x5f\\xdf\\x3f\\xbf\\x7f\\xff\"\n\nconst len8tab = \"\" +\n\t\"\\x00\\x01\\x02\\x02\\x03\\x03\\x03\\x03\\x04\\x04\\x04\\x04\\x04\\x04\\x04\\x04\" +\n\t\"\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\"\n"},{"name":"bitwise.gno","body":"// bitwise contains bitwise operations for Uint instances.\n// This file includes functions to perform bitwise AND, OR, XOR, and NOT operations, as well as bit shifting.\n// These operations are crucial for manipulating individual bits within a 256-bit unsigned integer.\npackage uint256\n\n// Or sets z = x | y and returns z.\nfunc (z *Uint) Or(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] | y.arr[0]\n\tz.arr[1] = x.arr[1] | y.arr[1]\n\tz.arr[2] = x.arr[2] | y.arr[2]\n\tz.arr[3] = x.arr[3] | y.arr[3]\n\treturn z\n}\n\n// And sets z = x \u0026 y and returns z.\nfunc (z *Uint) And(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026 y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026 y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026 y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026 y.arr[3]\n\treturn z\n}\n\n// Not sets z = ^x and returns z.\nfunc (z *Uint) Not(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = ^x.arr[3], ^x.arr[2], ^x.arr[1], ^x.arr[0]\n\treturn z\n}\n\n// AndNot sets z = x \u0026^ y and returns z.\nfunc (z *Uint) AndNot(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026^ y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026^ y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026^ y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026^ y.arr[3]\n\treturn z\n}\n\n// Xor sets z = x ^ y and returns z.\nfunc (z *Uint) Xor(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] ^ y.arr[0]\n\tz.arr[1] = x.arr[1] ^ y.arr[1]\n\tz.arr[2] = x.arr[2] ^ y.arr[2]\n\tz.arr[3] = x.arr[3] ^ y.arr[3]\n\treturn z\n}\n\n// Lsh sets z = x \u003c\u003c n and returns z.\nfunc (z *Uint) Lsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.lsh64(x)\n\t\tcase 128:\n\t\t\treturn z.lsh128(x)\n\t\tcase 192:\n\t\t\treturn z.lsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.lsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.lsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.lsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[0] \u003e\u003e (64 - n)\n\tz.arr[0] = z.arr[0] \u003c\u003c n\n\nsh64:\n\tb = z.arr[1] \u003e\u003e (64 - n)\n\tz.arr[1] = (z.arr[1] \u003c\u003c n) | a\n\nsh128:\n\ta = z.arr[2] \u003e\u003e (64 - n)\n\tz.arr[2] = (z.arr[2] \u003c\u003c n) | b\n\nsh192:\n\tz.arr[3] = (z.arr[3] \u003c\u003c n) | a\n\n\treturn z\n}\n\n// Rsh sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) Rsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.rsh64(x)\n\t\tcase 128:\n\t\t\treturn z.rsh128(x)\n\t\tcase 192:\n\t\t\treturn z.rsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.rsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.rsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.rsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[3] \u003c\u003c (64 - n)\n\tz.arr[3] = z.arr[3] \u003e\u003e n\n\nsh64:\n\tb = z.arr[2] \u003c\u003c (64 - n)\n\tz.arr[2] = (z.arr[2] \u003e\u003e n) | a\n\nsh128:\n\ta = z.arr[1] \u003c\u003c (64 - n)\n\tz.arr[1] = (z.arr[1] \u003e\u003e n) | b\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\n// SRsh (Signed/Arithmetic right shift)\n// considers z to be a signed integer, during right-shift\n// and sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) SRsh(x *Uint, n uint) *Uint {\n\t// If the MSB is 0, SRsh is same as Rsh.\n\tif !x.isBitSet(255) {\n\t\treturn z.Rsh(x, n)\n\t}\n\tif n%64 == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.srsh64(x)\n\t\tcase 128:\n\t\t\treturn z.srsh128(x)\n\t\tcase 192:\n\t\t\treturn z.srsh192(x)\n\t\tdefault:\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t}\n\tvar a uint64 = MaxUint64 \u003c\u003c (64 - n%64)\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t\tz.srsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.srsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.srsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\tz.arr[3], a = (z.arr[3]\u003e\u003en)|a, z.arr[3]\u003c\u003c(64-n)\n\nsh64:\n\tz.arr[2], a = (z.arr[2]\u003e\u003en)|a, z.arr[2]\u003c\u003c(64-n)\n\nsh128:\n\tz.arr[1], a = (z.arr[1]\u003e\u003en)|a, z.arr[1]\u003c\u003c(64-n)\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\nfunc (z *Uint) lsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[2], x.arr[1], x.arr[0], 0\n\treturn z\n}\n\nfunc (z *Uint) lsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[1], x.arr[0], 0, 0\n\treturn z\n}\n\nfunc (z *Uint) lsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[0], 0, 0, 0\n\treturn z\n}\n\nfunc (z *Uint) rsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) rsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) rsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x.arr[3]\n\treturn z\n}\n\nfunc (z *Uint) srsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) srsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) srsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, x.arr[3]\n\treturn z\n}\n"},{"name":"cmp.gno","body":"// cmp (or, comparisons) includes methods for comparing Uint instances.\n// These comparison functions cover a range of operations including equality checks, less than/greater than\n// evaluations, and specialized comparisons such as signed greater than. These are fundamental for logical\n// decision making based on Uint values.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Cmp compares z and x and returns:\n//\n//\t-1 if z \u003c  x\n//\t 0 if z == x\n//\t+1 if z \u003e  x\nfunc (z *Uint) Cmp(x *Uint) (r int) {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\td0, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\td1, carry := bits.Sub64(z.arr[1], x.arr[1], carry)\n\td2, carry := bits.Sub64(z.arr[2], x.arr[2], carry)\n\td3, carry := bits.Sub64(z.arr[3], x.arr[3], carry)\n\tif carry == 1 {\n\t\treturn -1\n\t}\n\tif d0|d1|d2|d3 == 0 {\n\t\treturn 0\n\t}\n\treturn 1\n}\n\n// IsZero returns true if z == 0\nfunc (z *Uint) IsZero() bool {\n\treturn (z.arr[0] | z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Sign returns:\n//\n//\t-1 if z \u003c  0\n//\t 0 if z == 0\n//\t+1 if z \u003e  0\n//\n// Where z is interpreted as a two's complement signed number\nfunc (z *Uint) Sign() int {\n\tif z.IsZero() {\n\t\treturn 0\n\t}\n\tif z.arr[3] \u003c 0x8000000000000000 {\n\t\treturn 1\n\t}\n\treturn -1\n}\n\n// LtUint64 returns true if z is smaller than n\nfunc (z *Uint) LtUint64(n uint64) bool {\n\treturn z.arr[0] \u003c n \u0026\u0026 (z.arr[1]|z.arr[2]|z.arr[3]) == 0\n}\n\n// GtUint64 returns true if z is larger than n\nfunc (z *Uint) GtUint64(n uint64) bool {\n\treturn z.arr[0] \u003e n || (z.arr[1]|z.arr[2]|z.arr[3]) != 0\n}\n\n// Lt returns true if z \u003c x\nfunc (z *Uint) Lt(x *Uint) bool {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\t_, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\t_, carry = bits.Sub64(z.arr[1], x.arr[1], carry)\n\t_, carry = bits.Sub64(z.arr[2], x.arr[2], carry)\n\t_, carry = bits.Sub64(z.arr[3], x.arr[3], carry)\n\n\treturn carry != 0\n}\n\n// Gt returns true if z \u003e x\nfunc (z *Uint) Gt(x *Uint) bool {\n\treturn x.Lt(z)\n}\n\n// Lte returns true if z \u003c= x\nfunc (z *Uint) Lte(x *Uint) bool {\n\tcond1 := z.Lt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Gte returns true if z \u003e= x\nfunc (z *Uint) Gte(x *Uint) bool {\n\tcond1 := z.Gt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Eq returns true if z == x\nfunc (z *Uint) Eq(x *Uint) bool {\n\treturn (z.arr[0] == x.arr[0]) \u0026\u0026 (z.arr[1] == x.arr[1]) \u0026\u0026 (z.arr[2] == x.arr[2]) \u0026\u0026 (z.arr[3] == x.arr[3])\n}\n\n// Neq returns true if z != x\nfunc (z *Uint) Neq(x *Uint) bool {\n\treturn !z.Eq(x)\n}\n\n// Sgt interprets z and x as signed integers, and returns\n// true if z \u003e x\nfunc (z *Uint) Sgt(x *Uint) bool {\n\tzSign := z.Sign()\n\txSign := x.Sign()\n\n\tswitch {\n\tcase zSign \u003e= 0 \u0026\u0026 xSign \u003c 0:\n\t\treturn true\n\tcase zSign \u003c 0 \u0026\u0026 xSign \u003e= 0:\n\t\treturn false\n\tdefault:\n\t\treturn z.Gt(x)\n\t}\n}\n"},{"name":"conversion.gno","body":"// conversions contains methods for converting Uint instances to other types and vice versa.\n// This includes conversions to and from basic types such as uint64 and int32, as well as string representations\n// and byte slices. Additionally, it covers marshaling and unmarshaling for JSON and other text formats.\npackage uint256\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Uint) Uint64() uint64 {\n\treturn z.arr[0]\n}\n\n// Uint64WithOverflow returns the lower 64-bits of z and bool whether overflow occurred\nfunc (z *Uint) Uint64WithOverflow() (uint64, bool) {\n\treturn z.arr[0], (z.arr[1] | z.arr[2] | z.arr[3]) != 0\n}\n\n// SetUint64 sets z to the value x\nfunc (z *Uint) SetUint64(x uint64) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x\n\treturn z\n}\n\n// IsUint64 reports whether z can be represented as a uint64.\nfunc (z *Uint) IsUint64() bool {\n\treturn (z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Dec returns the decimal representation of z.\nfunc (z *Uint) Dec() string {\n\tif z.IsZero() {\n\t\treturn \"0\"\n\t}\n\tif z.IsUint64() {\n\t\treturn strconv.FormatUint(z.Uint64(), 10)\n\t}\n\n\t// The max uint64 value being 18446744073709551615, the largest\n\t// power-of-ten below that is 10000000000000000000.\n\t// When we do a DivMod using that number, the remainder that we\n\t// get back is the lower part of the output.\n\t//\n\t// The ascii-output of remainder will never exceed 19 bytes (since it will be\n\t// below 10000000000000000000).\n\t//\n\t// Algorithm example using 100 as divisor\n\t//\n\t// 12345 % 100 = 45   (rem)\n\t// 12345 / 100 = 123  (quo)\n\t// -\u003e output '45', continue iterate on 123\n\tvar (\n\t\t// out is 98 bytes long: 78 (max size of a string without leading zeroes,\n\t\t// plus slack so we can copy 19 bytes every iteration).\n\t\t// We init it with zeroes, because when strconv appends the ascii representations,\n\t\t// it will omit leading zeroes.\n\t\tout     = []byte(\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\")\n\t\tdivisor = NewUint(10000000000000000000) // 20 digits\n\t\ty       = new(Uint).Set(z)              // copy to avoid modifying z\n\t\tpos     = len(out)                      // position to write to\n\t\tbuf     = make([]byte, 0, 19)           // buffer to write uint64:s to\n\t)\n\tfor {\n\t\t// Obtain Q and R for divisor\n\t\tvar quot Uint\n\t\trem := udivrem(quot.arr[:], y.arr[:], divisor)\n\t\ty.Set(\u0026quot) // Set Q for next loop\n\t\t// Convert the R to ascii representation\n\t\tbuf = strconv.AppendUint(buf[:0], rem.Uint64(), 10)\n\t\t// Copy in the ascii digits\n\t\tcopy(out[pos-len(buf):], buf)\n\t\tif y.IsZero() {\n\t\t\tbreak\n\t\t}\n\t\t// Move 19 digits left\n\t\tpos -= 19\n\t}\n\t// skip leading zeroes by only using the 'used size' of buf\n\treturn string(out[pos-len(buf):])\n}\n\nfunc (z *Uint) Scan(src any) error {\n\tif src == nil {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tswitch src := src.(type) {\n\tcase string:\n\t\treturn z.scanScientificFromString(src)\n\tcase []byte:\n\t\treturn z.scanScientificFromString(string(src))\n\t}\n\treturn errors.New(\"default // unsupported type: can't convert to uint256.Uint\")\n}\n\nfunc (z *Uint) scanScientificFromString(src string) error {\n\tif len(src) == 0 {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tidx := strings.IndexByte(src, 'e')\n\tif idx == -1 {\n\t\treturn z.SetFromDecimal(src)\n\t}\n\tif err := z.SetFromDecimal(src[:idx]); err != nil {\n\t\treturn err\n\t}\n\tif src[(idx+1):] == \"0\" {\n\t\treturn nil\n\t}\n\texp := new(Uint)\n\tif err := exp.SetFromDecimal(src[(idx + 1):]); err != nil {\n\t\treturn err\n\t}\n\tif exp.GtUint64(77) { // 10**78 is larger than 2**256\n\t\treturn ErrBig256Range\n\t}\n\texp.Exp(NewUint(10), exp)\n\tif _, overflow := z.MulOverflow(z, exp); overflow {\n\t\treturn ErrBig256Range\n\t}\n\treturn nil\n}\n\n// ToString returns the decimal string representation of z. It returns an empty string if z is nil.\n// OBS: doesn't exist from holiman's uint256\nfunc (z *Uint) String() string {\n\tif z == nil {\n\t\treturn \"\"\n\t}\n\n\treturn z.Dec()\n}\n\n// MarshalJSON implements json.Marshaler.\n// MarshalJSON marshals using the 'decimal string' representation. This is _not_ compatible\n// with big.Uint: big.Uint marshals into JSON 'native' numeric format.\n//\n// The JSON  native format is, on some platforms, (e.g. javascript), limited to 53-bit large\n// integer space. Thus, U256 uses string-format, which is not compatible with\n// big.int (big.Uint refuses to unmarshal a string representation).\nfunc (z *Uint) MarshalJSON() ([]byte, error) {\n\treturn []byte(`\"` + z.Dec() + `\"`), nil\n}\n\n// UnmarshalJSON implements json.Unmarshaler. UnmarshalJSON accepts either\n// - Quoted string: either hexadecimal OR decimal\n// - Not quoted string: only decimal\nfunc (z *Uint) UnmarshalJSON(input []byte) error {\n\tif len(input) \u003c 2 || input[0] != '\"' || input[len(input)-1] != '\"' {\n\t\t// if not quoted, it must be decimal\n\t\treturn z.fromDecimal(string(input))\n\t}\n\treturn z.UnmarshalText(input[1 : len(input)-1])\n}\n\n// MarshalText implements encoding.TextMarshaler\n// MarshalText marshals using the decimal representation (compatible with big.Uint)\nfunc (z *Uint) MarshalText() ([]byte, error) {\n\treturn []byte(z.Dec()), nil\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler. This method\n// can unmarshal either hexadecimal or decimal.\n// - For hexadecimal, the input _must_ be prefixed with 0x or 0X\nfunc (z *Uint) UnmarshalText(input []byte) error {\n\tif len(input) \u003e= 2 \u0026\u0026 input[0] == '0' \u0026\u0026 (input[1] == 'x' || input[1] == 'X') {\n\t\treturn z.fromHex(string(input))\n\t}\n\treturn z.fromDecimal(string(input))\n}\n\n// SetBytes interprets buf as the bytes of a big-endian unsigned\n// integer, sets z to that value, and returns z.\n// If buf is larger than 32 bytes, the last 32 bytes is used.\nfunc (z *Uint) SetBytes(buf []byte) *Uint {\n\tswitch l := len(buf); l {\n\tcase 0:\n\t\tz.Clear()\n\tcase 1:\n\t\tz.SetBytes1(buf)\n\tcase 2:\n\t\tz.SetBytes2(buf)\n\tcase 3:\n\t\tz.SetBytes3(buf)\n\tcase 4:\n\t\tz.SetBytes4(buf)\n\tcase 5:\n\t\tz.SetBytes5(buf)\n\tcase 6:\n\t\tz.SetBytes6(buf)\n\tcase 7:\n\t\tz.SetBytes7(buf)\n\tcase 8:\n\t\tz.SetBytes8(buf)\n\tcase 9:\n\t\tz.SetBytes9(buf)\n\tcase 10:\n\t\tz.SetBytes10(buf)\n\tcase 11:\n\t\tz.SetBytes11(buf)\n\tcase 12:\n\t\tz.SetBytes12(buf)\n\tcase 13:\n\t\tz.SetBytes13(buf)\n\tcase 14:\n\t\tz.SetBytes14(buf)\n\tcase 15:\n\t\tz.SetBytes15(buf)\n\tcase 16:\n\t\tz.SetBytes16(buf)\n\tcase 17:\n\t\tz.SetBytes17(buf)\n\tcase 18:\n\t\tz.SetBytes18(buf)\n\tcase 19:\n\t\tz.SetBytes19(buf)\n\tcase 20:\n\t\tz.SetBytes20(buf)\n\tcase 21:\n\t\tz.SetBytes21(buf)\n\tcase 22:\n\t\tz.SetBytes22(buf)\n\tcase 23:\n\t\tz.SetBytes23(buf)\n\tcase 24:\n\t\tz.SetBytes24(buf)\n\tcase 25:\n\t\tz.SetBytes25(buf)\n\tcase 26:\n\t\tz.SetBytes26(buf)\n\tcase 27:\n\t\tz.SetBytes27(buf)\n\tcase 28:\n\t\tz.SetBytes28(buf)\n\tcase 29:\n\t\tz.SetBytes29(buf)\n\tcase 30:\n\t\tz.SetBytes30(buf)\n\tcase 31:\n\t\tz.SetBytes31(buf)\n\tdefault:\n\t\tz.SetBytes32(buf[l-32:])\n\t}\n\treturn z\n}\n\n// SetBytes1 is identical to SetBytes(in[:1]), but panics is input is too short\nfunc (z *Uint) SetBytes1(in []byte) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(in[0])\n\treturn z\n}\n\n// SetBytes2 is identical to SetBytes(in[:2]), but panics is input is too short\nfunc (z *Uint) SetBytes2(in []byte) *Uint {\n\t_ = in[1] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\treturn z\n}\n\n// SetBytes3 is identical to SetBytes(in[:3]), but panics is input is too short\nfunc (z *Uint) SetBytes3(in []byte) *Uint {\n\t_ = in[2] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\treturn z\n}\n\n// SetBytes4 is identical to SetBytes(in[:4]), but panics is input is too short\nfunc (z *Uint) SetBytes4(in []byte) *Uint {\n\t_ = in[3] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\treturn z\n}\n\n// SetBytes5 is identical to SetBytes(in[:5]), but panics is input is too short\nfunc (z *Uint) SetBytes5(in []byte) *Uint {\n\t_ = in[4] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint40(in[0:5])\n\treturn z\n}\n\n// SetBytes6 is identical to SetBytes(in[:6]), but panics is input is too short\nfunc (z *Uint) SetBytes6(in []byte) *Uint {\n\t_ = in[5] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint48(in[0:6])\n\treturn z\n}\n\n// SetBytes7 is identical to SetBytes(in[:7]), but panics is input is too short\nfunc (z *Uint) SetBytes7(in []byte) *Uint {\n\t_ = in[6] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint56(in[0:7])\n\treturn z\n}\n\n// SetBytes8 is identical to SetBytes(in[:8]), but panics is input is too short\nfunc (z *Uint) SetBytes8(in []byte) *Uint {\n\t_ = in[7] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = binary.BigEndian.Uint64(in[0:8])\n\treturn z\n}\n\n// SetBytes9 is identical to SetBytes(in[:9]), but panics is input is too short\nfunc (z *Uint) SetBytes9(in []byte) *Uint {\n\t_ = in[8] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(in[0])\n\tz.arr[0] = binary.BigEndian.Uint64(in[1:9])\n\treturn z\n}\n\n// SetBytes10 is identical to SetBytes(in[:10]), but panics is input is too short\nfunc (z *Uint) SetBytes10(in []byte) *Uint {\n\t_ = in[9] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[2:10])\n\treturn z\n}\n\n// SetBytes11 is identical to SetBytes(in[:11]), but panics is input is too short\nfunc (z *Uint) SetBytes11(in []byte) *Uint {\n\t_ = in[10] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[0] = binary.BigEndian.Uint64(in[3:11])\n\treturn z\n}\n\n// SetBytes12 is identical to SetBytes(in[:12]), but panics is input is too short\nfunc (z *Uint) SetBytes12(in []byte) *Uint {\n\t_ = in[11] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[4:12])\n\treturn z\n}\n\n// SetBytes13 is identical to SetBytes(in[:13]), but panics is input is too short\nfunc (z *Uint) SetBytes13(in []byte) *Uint {\n\t_ = in[12] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint40(in[0:5])\n\tz.arr[0] = binary.BigEndian.Uint64(in[5:13])\n\treturn z\n}\n\n// SetBytes14 is identical to SetBytes(in[:14]), but panics is input is too short\nfunc (z *Uint) SetBytes14(in []byte) *Uint {\n\t_ = in[13] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint48(in[0:6])\n\tz.arr[0] = binary.BigEndian.Uint64(in[6:14])\n\treturn z\n}\n\n// SetBytes15 is identical to SetBytes(in[:15]), but panics is input is too short\nfunc (z *Uint) SetBytes15(in []byte) *Uint {\n\t_ = in[14] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint56(in[0:7])\n\tz.arr[0] = binary.BigEndian.Uint64(in[7:15])\n\treturn z\n}\n\n// SetBytes16 is identical to SetBytes(in[:16]), but panics is input is too short\nfunc (z *Uint) SetBytes16(in []byte) *Uint {\n\t_ = in[15] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[0] = binary.BigEndian.Uint64(in[8:16])\n\treturn z\n}\n\n// SetBytes17 is identical to SetBytes(in[:17]), but panics is input is too short\nfunc (z *Uint) SetBytes17(in []byte) *Uint {\n\t_ = in[16] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(in[0])\n\tz.arr[1] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[0] = binary.BigEndian.Uint64(in[9:17])\n\treturn z\n}\n\n// SetBytes18 is identical to SetBytes(in[:18]), but panics is input is too short\nfunc (z *Uint) SetBytes18(in []byte) *Uint {\n\t_ = in[17] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[0] = binary.BigEndian.Uint64(in[10:18])\n\treturn z\n}\n\n// SetBytes19 is identical to SetBytes(in[:19]), but panics is input is too short\nfunc (z *Uint) SetBytes19(in []byte) *Uint {\n\t_ = in[18] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[1] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[0] = binary.BigEndian.Uint64(in[11:19])\n\treturn z\n}\n\n// SetBytes20 is identical to SetBytes(in[:20]), but panics is input is too short\nfunc (z *Uint) SetBytes20(in []byte) *Uint {\n\t_ = in[19] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[0] = binary.BigEndian.Uint64(in[12:20])\n\treturn z\n}\n\n// SetBytes21 is identical to SetBytes(in[:21]), but panics is input is too short\nfunc (z *Uint) SetBytes21(in []byte) *Uint {\n\t_ = in[20] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint40(in[0:5])\n\tz.arr[1] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[0] = binary.BigEndian.Uint64(in[13:21])\n\treturn z\n}\n\n// SetBytes22 is identical to SetBytes(in[:22]), but panics is input is too short\nfunc (z *Uint) SetBytes22(in []byte) *Uint {\n\t_ = in[21] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint48(in[0:6])\n\tz.arr[1] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[0] = binary.BigEndian.Uint64(in[14:22])\n\treturn z\n}\n\n// SetBytes23 is identical to SetBytes(in[:23]), but panics is input is too short\nfunc (z *Uint) SetBytes23(in []byte) *Uint {\n\t_ = in[22] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint56(in[0:7])\n\tz.arr[1] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[0] = binary.BigEndian.Uint64(in[15:23])\n\treturn z\n}\n\n// SetBytes24 is identical to SetBytes(in[:24]), but panics is input is too short\nfunc (z *Uint) SetBytes24(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[1] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[0] = binary.BigEndian.Uint64(in[16:24])\n\treturn z\n}\n\n// SetBytes25 is identical to SetBytes(in[:25]), but panics is input is too short\nfunc (z *Uint) SetBytes25(in []byte) *Uint {\n\t_ = in[24] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(in[0])\n\tz.arr[2] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[1] = binary.BigEndian.Uint64(in[9:17])\n\tz.arr[0] = binary.BigEndian.Uint64(in[17:25])\n\treturn z\n}\n\n// SetBytes26 is identical to SetBytes(in[:26]), but panics is input is too short\nfunc (z *Uint) SetBytes26(in []byte) *Uint {\n\t_ = in[25] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[1] = binary.BigEndian.Uint64(in[10:18])\n\tz.arr[0] = binary.BigEndian.Uint64(in[18:26])\n\treturn z\n}\n\n// SetBytes27 is identical to SetBytes(in[:27]), but panics is input is too short\nfunc (z *Uint) SetBytes27(in []byte) *Uint {\n\t_ = in[26] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[2] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[1] = binary.BigEndian.Uint64(in[11:19])\n\tz.arr[0] = binary.BigEndian.Uint64(in[19:27])\n\treturn z\n}\n\n// SetBytes28 is identical to SetBytes(in[:28]), but panics is input is too short\nfunc (z *Uint) SetBytes28(in []byte) *Uint {\n\t_ = in[27] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[1] = binary.BigEndian.Uint64(in[12:20])\n\tz.arr[0] = binary.BigEndian.Uint64(in[20:28])\n\treturn z\n}\n\n// SetBytes29 is identical to SetBytes(in[:29]), but panics is input is too short\nfunc (z *Uint) SetBytes29(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint40(in[0:5])\n\tz.arr[2] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[1] = binary.BigEndian.Uint64(in[13:21])\n\tz.arr[0] = binary.BigEndian.Uint64(in[21:29])\n\treturn z\n}\n\n// SetBytes30 is identical to SetBytes(in[:30]), but panics is input is too short\nfunc (z *Uint) SetBytes30(in []byte) *Uint {\n\t_ = in[29] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint48(in[0:6])\n\tz.arr[2] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[1] = binary.BigEndian.Uint64(in[14:22])\n\tz.arr[0] = binary.BigEndian.Uint64(in[22:30])\n\treturn z\n}\n\n// SetBytes31 is identical to SetBytes(in[:31]), but panics is input is too short\nfunc (z *Uint) SetBytes31(in []byte) *Uint {\n\t_ = in[30] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint56(in[0:7])\n\tz.arr[2] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[1] = binary.BigEndian.Uint64(in[15:23])\n\tz.arr[0] = binary.BigEndian.Uint64(in[23:31])\n\treturn z\n}\n\n// SetBytes32 sets z to the value of the big-endian 256-bit unsigned integer in.\nfunc (z *Uint) SetBytes32(in []byte) *Uint {\n\t_ = in[31] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[2] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[1] = binary.BigEndian.Uint64(in[16:24])\n\tz.arr[0] = binary.BigEndian.Uint64(in[24:32])\n\treturn z\n}\n\n// Utility methods that are \"missing\" among the bigEndian.UintXX methods.\n\n// bigEndianUint40 returns the uint64 value represented by the 5 bytes in big-endian order.\nfunc bigEndianUint40(b []byte) uint64 {\n\t_ = b[4] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[4]) | uint64(b[3])\u003c\u003c8 | uint64(b[2])\u003c\u003c16 | uint64(b[1])\u003c\u003c24 |\n\t\tuint64(b[0])\u003c\u003c32\n}\n\n// bigEndianUint56 returns the uint64 value represented by the 7 bytes in big-endian order.\nfunc bigEndianUint56(b []byte) uint64 {\n\t_ = b[6] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[6]) | uint64(b[5])\u003c\u003c8 | uint64(b[4])\u003c\u003c16 | uint64(b[3])\u003c\u003c24 |\n\t\tuint64(b[2])\u003c\u003c32 | uint64(b[1])\u003c\u003c40 | uint64(b[0])\u003c\u003c48\n}\n\n// bigEndianUint48 returns the uint64 value represented by the 6 bytes in big-endian order.\nfunc bigEndianUint48(b []byte) uint64 {\n\t_ = b[5] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[5]) | uint64(b[4])\u003c\u003c8 | uint64(b[3])\u003c\u003c16 | uint64(b[2])\u003c\u003c24 |\n\t\tuint64(b[1])\u003c\u003c32 | uint64(b[0])\u003c\u003c40\n}\n"},{"name":"error.gno","body":"package uint256\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrEmptyString      = errors.New(\"empty hex string\")\n\tErrSyntax           = errors.New(\"invalid hex string\")\n\tErrRange            = errors.New(\"number out of range\")\n\tErrMissingPrefix    = errors.New(\"hex string without 0x prefix\")\n\tErrEmptyNumber      = errors.New(\"hex string \\\"0x\\\"\")\n\tErrLeadingZero      = errors.New(\"hex number with leading zero digits\")\n\tErrBig256Range      = errors.New(\"hex number \u003e 256 bits\")\n\tErrBadBufferLength  = errors.New(\"bad ssz buffer length\")\n\tErrBadEncodedLength = errors.New(\"bad ssz encoded length\")\n\tErrInvalidBase      = errors.New(\"invalid base\")\n\tErrInvalidBitSize   = errors.New(\"invalid bit size\")\n)\n\ntype u256Error struct {\n\tfn    string // function name\n\tinput string\n\terr   error\n}\n\nfunc (e *u256Error) Error() string {\n\treturn e.fn + \": \" + e.input + \": \" + e.err.Error()\n}\n\nfunc (e *u256Error) Unwrap() error {\n\treturn e.err\n}\n\nfunc errEmptyString(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyString}\n}\n\nfunc errSyntax(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrSyntax}\n}\n\nfunc errMissingPrefix(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrMissingPrefix}\n}\n\nfunc errEmptyNumber(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyNumber}\n}\n\nfunc errLeadingZero(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrLeadingZero}\n}\n\nfunc errRange(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrRange}\n}\n\nfunc errBig256Range(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBig256Range}\n}\n\nfunc errBadBufferLength(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBadBufferLength}\n}\n\nfunc errInvalidBase(fn string, base int) error {\n\treturn \u0026u256Error{fn: fn, input: string(base), err: ErrInvalidBase}\n}\n\nfunc errInvalidBitSize(fn string, bitSize int) error {\n\treturn \u0026u256Error{fn: fn, input: string(bitSize), err: ErrInvalidBitSize}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/onbloc/uint256\"\ngno = \"0.9\"\n"},{"name":"mod.gno","body":"package uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Some utility functions\n\n// Reciprocal computes a 320-bit value representing 1/m\n//\n// Notes:\n// - specialized for m.arr[3] != 0, hence limited to 2^192 \u003c= m \u003c 2^256\n// - returns zero if m.arr[3] == 0\n// - starts with a 32-bit division, refines with newton-raphson iterations\nfunc Reciprocal(m *Uint) (mu [5]uint64) {\n\tif m.arr[3] == 0 {\n\t\treturn mu\n\t}\n\n\ts := bits.LeadingZeros64(m.arr[3]) // Replace with leadingZeros(m) for general case\n\tp := 255 - s                       // floor(log_2(m)), m\u003e0\n\n\t// 0 or a power of 2?\n\n\t// Check if at least one bit is set in m.arr[2], m.arr[1] or m.arr[0],\n\t// or at least two bits in m.arr[3]\n\n\tif m.arr[0]|m.arr[1]|m.arr[2]|(m.arr[3]\u0026(m.arr[3]-1)) == 0 {\n\n\t\tmu[4] = ^uint64(0) \u003e\u003e uint(p\u002663)\n\t\tmu[3] = ^uint64(0)\n\t\tmu[2] = ^uint64(0)\n\t\tmu[1] = ^uint64(0)\n\t\tmu[0] = ^uint64(0)\n\n\t\treturn mu\n\t}\n\n\t// Maximise division precision by left-aligning divisor\n\n\tvar (\n\t\ty  Uint   // left-aligned copy of m\n\t\tr0 uint32 // estimate of 2^31/y\n\t)\n\n\ty.Lsh(m, uint(s)) // 1/2 \u003c y \u003c 1\n\n\t// Extract most significant 32 bits\n\n\tyh := uint32(y.arr[3] \u003e\u003e 32)\n\n\tif yh == 0x80000000 { // Avoid overflow in division\n\t\tr0 = 0xffffffff\n\t} else {\n\t\tr0, _ = bits.Div32(0x80000000, 0, yh)\n\t}\n\n\t// First iteration: 32 -\u003e 64\n\n\tt1 := uint64(r0)                 // 2^31/y\n\tt1 *= t1                         // 2^62/y^2\n\tt1, _ = bits.Mul64(t1, y.arr[3]) // 2^62/y^2 * 2^64/y / 2^64 = 2^62/y\n\n\tr1 := uint64(r0) \u003c\u003c 32 // 2^63/y\n\tr1 -= t1               // 2^63/y - 2^62/y = 2^62/y\n\tr1 *= 2                // 2^63/y\n\n\tif (r1 | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr1 = ^uint64(0)\n\t}\n\n\t// Second iteration: 64 -\u003e 128\n\n\t// square: 2^126/y^2\n\ta2h, a2l := bits.Mul64(r1, r1)\n\n\t// multiply by y: e2h:e2l:b2h = 2^126/y^2 * 2^128/y / 2^128 = 2^126/y\n\tb2h, _ := bits.Mul64(a2l, y.arr[2])\n\tc2h, c2l := bits.Mul64(a2l, y.arr[3])\n\td2h, d2l := bits.Mul64(a2h, y.arr[2])\n\te2h, e2l := bits.Mul64(a2h, y.arr[3])\n\n\tb2h, c := bits.Add64(b2h, c2l, 0)\n\te2l, c = bits.Add64(e2l, c2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t_, c = bits.Add64(b2h, d2l, 0)\n\te2l, c = bits.Add64(e2l, d2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t// subtract: t2h:t2l = 2^127/y - 2^126/y = 2^126/y\n\tt2l, b := bits.Sub64(0, e2l, 0)\n\tt2h, _ := bits.Sub64(r1, e2h, b)\n\n\t// double: r2h:r2l = 2^127/y\n\tr2l, c := bits.Add64(t2l, t2l, 0)\n\tr2h, _ := bits.Add64(t2h, t2h, c)\n\n\tif (r2h | r2l | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr2h = ^uint64(0)\n\t\tr2l = ^uint64(0)\n\t}\n\n\t// Third iteration: 128 -\u003e 192\n\n\t// square r2 (keep 256 bits): 2^190/y^2\n\ta3h, a3l := bits.Mul64(r2l, r2l)\n\tb3h, b3l := bits.Mul64(r2l, r2h)\n\tc3h, c3l := bits.Mul64(r2h, r2h)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\t// multiply by y: q = 2^190/y^2 * 2^192/y / 2^192 = 2^190/y\n\n\tx0 := a3l\n\tx1 := a3h\n\tx2 := c3l\n\tx3 := c3h\n\n\tvar q0, q1, q2, q3, q4, t0 uint64\n\n\tq0, _ = bits.Mul64(x2, y.arr[0])\n\tq1, t0 = bits.Mul64(x3, y.arr[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x1, y.arr[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x3, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x3, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, y.arr[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x3, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\t// subtract: t3 = 2^191/y - 2^190/y = 2^190/y\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\tt3l, b := bits.Sub64(0, q2, b)\n\tt3m, b := bits.Sub64(r2l, q3, b)\n\tt3h, _ := bits.Sub64(r2h, q4, b)\n\n\t// double: r3 = 2^191/y\n\tr3l, c := bits.Add64(t3l, t3l, 0)\n\tr3m, c := bits.Add64(t3m, t3m, c)\n\tr3h, _ := bits.Add64(t3h, t3h, c)\n\n\t// Fourth iteration: 192 -\u003e 320\n\n\t// square r3\n\n\ta4h, a4l := bits.Mul64(r3l, r3l)\n\tb4h, b4l := bits.Mul64(r3l, r3m)\n\tc4h, c4l := bits.Mul64(r3l, r3h)\n\td4h, d4l := bits.Mul64(r3m, r3m)\n\te4h, e4l := bits.Mul64(r3m, r3h)\n\tf4h, f4l := bits.Mul64(r3h, r3h)\n\n\tb4h, c = bits.Add64(b4h, c4l, 0)\n\te4l, c = bits.Add64(e4l, c4h, c)\n\te4h, _ = bits.Add64(e4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\t// multiply by y\n\n\tx1, x0 = bits.Mul64(d4h, y.arr[0])\n\tx3, x2 = bits.Mul64(f4h, y.arr[0])\n\tt1, t0 = bits.Mul64(f4l, y.arr[0])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx3, _ = bits.Add64(x3, 0, c)\n\n\tt1, t0 = bits.Mul64(d4h, y.arr[1])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx4, t0 := bits.Mul64(f4h, y.arr[1])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[1])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[1])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[2])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[2])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx5, t0 := bits.Mul64(f4h, y.arr[2])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[2])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[2])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[3])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[3])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx6, t0 := bits.Mul64(f4h, y.arr[3])\n\tx5, c = bits.Add64(x5, t0, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\tt1, t0 = bits.Mul64(a4l, y.arr[3])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[3])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[3])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, c = bits.Add64(x5, t1, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\n\t// subtract\n\t_, b = bits.Sub64(0, x0, 0)\n\t_, b = bits.Sub64(0, x1, b)\n\tr4l, b := bits.Sub64(0, x2, b)\n\tr4k, b := bits.Sub64(0, x3, b)\n\tr4j, b := bits.Sub64(r3l, x4, b)\n\tr4i, b := bits.Sub64(r3m, x5, b)\n\tr4h, _ := bits.Sub64(r3h, x6, b)\n\n\t// Multiply candidate for 1/4y by y, with full precision\n\n\tx0 = r4l\n\tx1 = r4k\n\tx2 = r4j\n\tx3 = r4i\n\tx4 = r4h\n\n\tq1, q0 = bits.Mul64(x0, y.arr[0])\n\tq3, q2 = bits.Mul64(x2, y.arr[0])\n\tq5, q4 := bits.Mul64(x4, y.arr[0])\n\n\tt1, t0 = bits.Mul64(x1, y.arr[0])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[0])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq6, t0 := bits.Mul64(x4, y.arr[1])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[1])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[1])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq7, t0 := bits.Mul64(x4, y.arr[2])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[2])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq8, t0 := bits.Mul64(x4, y.arr[3])\n\tq7, c = bits.Add64(q7, t0, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq4, c = bits.Add64(q4, t0, 0)\n\tq5, c = bits.Add64(q5, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[3])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, c = bits.Add64(q7, t1, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\t// Final adjustment\n\n\t// subtract q from 1/4\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\t_, b = bits.Sub64(0, q2, b)\n\t_, b = bits.Sub64(0, q3, b)\n\t_, b = bits.Sub64(0, q4, b)\n\t_, b = bits.Sub64(0, q5, b)\n\t_, b = bits.Sub64(0, q6, b)\n\t_, b = bits.Sub64(0, q7, b)\n\t_, b = bits.Sub64(uint64(1)\u003c\u003c62, q8, b)\n\n\t// decrement the result\n\tx0, t := bits.Sub64(r4l, 1, 0)\n\tx1, t = bits.Sub64(r4k, 0, t)\n\tx2, t = bits.Sub64(r4j, 0, t)\n\tx3, t = bits.Sub64(r4i, 0, t)\n\tx4, _ = bits.Sub64(r4h, 0, t)\n\n\t// commit the decrement if the subtraction underflowed (reciprocal was too large)\n\tif b != 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\t// Shift to correct bit alignment, truncating excess bits\n\n\tp = (p \u0026 63) - 1\n\n\tx0, c = bits.Add64(r4l, r4l, 0)\n\tx1, c = bits.Add64(r4k, r4k, c)\n\tx2, c = bits.Add64(r4j, r4j, c)\n\tx3, c = bits.Add64(r4i, r4i, c)\n\tx4, _ = bits.Add64(r4h, r4h, c)\n\n\tif p \u003c 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t\tp = 0 // avoid negative shift below\n\t}\n\n\t{\n\t\tr := uint(p)      // right shift\n\t\tl := uint(64 - r) // left shift\n\n\t\tx0 = (r4l \u003e\u003e r) | (r4k \u003c\u003c l)\n\t\tx1 = (r4k \u003e\u003e r) | (r4j \u003c\u003c l)\n\t\tx2 = (r4j \u003e\u003e r) | (r4i \u003c\u003c l)\n\t\tx3 = (r4i \u003e\u003e r) | (r4h \u003c\u003c l)\n\t\tx4 = (r4h \u003e\u003e r)\n\t}\n\n\tif p \u003e 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\tmu[0] = r4l\n\tmu[1] = r4k\n\tmu[2] = r4j\n\tmu[3] = r4i\n\tmu[4] = r4h\n\n\treturn mu\n}\n\n// reduce4 computes the least non-negative residue of x modulo m\n//\n// requires a four-word modulus (m.arr[3] \u003e 1) and its inverse (mu)\nfunc reduce4(x [8]uint64, m *Uint, mu [5]uint64) (z Uint) {\n\t// NB: Most variable names in the comments match the pseudocode for\n\t// \tBarrett reduction in the Handbook of Applied Cryptography.\n\n\t// q1 = x/2^192\n\n\tx0 := x[3]\n\tx1 := x[4]\n\tx2 := x[5]\n\tx3 := x[6]\n\tx4 := x[7]\n\n\t// q2 = q1 * mu; q3 = q2 / 2^320\n\n\tvar q0, q1, q2, q3, q4, q5, t0, t1, c uint64\n\n\tq0, _ = bits.Mul64(x3, mu[0])\n\tq1, t0 = bits.Mul64(x4, mu[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x2, mu[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x4, mu[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x3, mu[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, mu[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x4, mu[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x1, mu[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x3, mu[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, mu[3])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, mu[3])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x4, mu[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, mu[4])\n\t_, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, mu[4])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq5, t0 = bits.Mul64(x4, mu[4])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[4])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[4])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\t// Drop the fractional part of q3\n\n\tq0 = q1\n\tq1 = q2\n\tq2 = q3\n\tq3 = q4\n\tq4 = q5\n\n\t// r1 = x mod 2^320\n\n\tx0 = x[0]\n\tx1 = x[1]\n\tx2 = x[2]\n\tx3 = x[3]\n\tx4 = x[4]\n\n\t// r2 = q3 * m mod 2^320\n\n\tvar r0, r1, r2, r3, r4 uint64\n\n\tr4, r3 = bits.Mul64(q0, m.arr[3])\n\t_, t0 = bits.Mul64(q1, m.arr[3])\n\tr4, _ = bits.Add64(r4, t0, 0)\n\n\tt1, r2 = bits.Mul64(q0, m.arr[2])\n\tr3, c = bits.Add64(r3, t1, 0)\n\t_, t0 = bits.Mul64(q2, m.arr[2])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[2])\n\tr3, c = bits.Add64(r3, t0, 0)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, r1 = bits.Mul64(q0, m.arr[1])\n\tr2, c = bits.Add64(r2, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[1])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[1])\n\tr2, c = bits.Add64(r2, t0, 0)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q3, m.arr[1])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, r0 = bits.Mul64(q0, m.arr[0])\n\tr1, c = bits.Add64(r1, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[0])\n\tr2, c = bits.Add64(r2, t0, c)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q4, m.arr[0])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[0])\n\tr1, c = bits.Add64(r1, t0, 0)\n\tr2, c = bits.Add64(r2, t1, c)\n\tt1, t0 = bits.Mul64(q3, m.arr[0])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\t// r = r1 - r2\n\n\tvar b uint64\n\n\tr0, b = bits.Sub64(x0, r0, 0)\n\tr1, b = bits.Sub64(x1, r1, b)\n\tr2, b = bits.Sub64(x2, r2, b)\n\tr3, b = bits.Sub64(x3, r3, b)\n\tr4, b = bits.Sub64(x4, r4, b)\n\n\t// if r\u003c0 then r+=m\n\n\tif b != 0 {\n\t\tr0, c = bits.Add64(r0, m.arr[0], 0)\n\t\tr1, c = bits.Add64(r1, m.arr[1], c)\n\t\tr2, c = bits.Add64(r2, m.arr[2], c)\n\t\tr3, c = bits.Add64(r3, m.arr[3], c)\n\t\tr4, _ = bits.Add64(r4, 0, c)\n\t}\n\n\t// while (r\u003e=m) r-=m\n\n\tfor {\n\t\t// q = r - m\n\t\tq0, b = bits.Sub64(r0, m.arr[0], 0)\n\t\tq1, b = bits.Sub64(r1, m.arr[1], b)\n\t\tq2, b = bits.Sub64(r2, m.arr[2], b)\n\t\tq3, b = bits.Sub64(r3, m.arr[3], b)\n\t\tq4, b = bits.Sub64(r4, 0, b)\n\n\t\t// if borrow break\n\t\tif b != 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// r = q\n\t\tr4, r3, r2, r1, r0 = q4, q3, q2, q1, q0\n\t}\n\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = r3, r2, r1, r0\n\n\treturn z\n}\n"},{"name":"uint256.gno","body":"// Ported from https://github.com/holiman/uint256\n// This package provides a 256-bit unsigned integer type, Uint256, and associated functions.\npackage uint256\n\nimport (\n\t\"errors\"\n\t\"math/bits\"\n\t\"strconv\"\n)\n\nconst (\n\tMaxUint64 = 1\u003c\u003c64 - 1\n\tuintSize  = 32 \u003c\u003c (^uint(0) \u003e\u003e 63)\n)\n\n// Uint is represented as an array of 4 uint64, in little-endian order,\n// so that Uint[3] is the most significant, and Uint[0] is the least significant\ntype Uint struct {\n\tarr [4]uint64\n}\n\n// NewUint returns a new initialized Uint.\nfunc NewUint(val uint64) *Uint {\n\tz := \u0026Uint{arr: [4]uint64{val, 0, 0, 0}}\n\treturn z\n}\n\n// Zero returns a new Uint initialized to zero.\nfunc Zero() *Uint {\n\treturn NewUint(0)\n}\n\n// One returns a new Uint initialized to one.\nfunc One() *Uint {\n\treturn NewUint(1)\n}\n\n// SetAllOne sets all the bits of z to 1\nfunc (z *Uint) SetAllOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, MaxUint64\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Uint) Set(x *Uint) *Uint {\n\t*z = *x\n\n\treturn z\n}\n\n// SetOne sets z to 1\nfunc (z *Uint) SetOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 1\n\treturn z\n}\n\nconst twoPow256Sub1 = \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n\n// SetFromDecimal sets z from the given string, interpreted as a decimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Uint).SetString(..., 10) method.\n// Notable differences:\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0\",\n//   - (this method does not accept any negative input as valid))\nfunc (z *Uint) SetFromDecimal(s string) (err error) {\n\t// Remove max one leading +\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '+' {\n\t\ts = s[1:]\n\t}\n\t// Remove any number of leading zeroes\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '0' {\n\t\tvar i int\n\t\tvar c rune\n\t\tfor i, c = range s {\n\t\t\tif c != '0' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ts = s[i:]\n\t}\n\tif len(s) \u003c len(twoPow256Sub1) {\n\t\treturn z.fromDecimal(s)\n\t}\n\tif len(s) == len(twoPow256Sub1) {\n\t\tif s \u003e twoPow256Sub1 {\n\t\t\treturn ErrBig256Range\n\t\t}\n\t\treturn z.fromDecimal(s)\n\t}\n\treturn ErrBig256Range\n}\n\n// FromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string. Numbers larger than 256 bits are not accepted.\nfunc FromDecimal(decimal string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromDecimal(decimal string) *Uint {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// multipliers holds the values that are needed for fromDecimal\nvar multipliers = [5]*Uint{\n\tnil, // represents first round, no multiplication needed\n\t{[4]uint64{10000000000000000000, 0, 0, 0}},                                     // 10 ^ 19\n\t{[4]uint64{687399551400673280, 5421010862427522170, 0, 0}},                     // 10 ^ 38\n\t{[4]uint64{5332261958806667264, 17004971331911604867, 2938735877055718769, 0}}, // 10 ^ 57\n\t{[4]uint64{0, 8607968719199866880, 532749306367912313, 1593091911132452277}},   // 10 ^ 76\n}\n\n// fromDecimal is a helper function to only ever be called via SetFromDecimal\n// this function takes a string and chunks it up, calling ParseUint on it up to 5 times\n// these chunks are then multiplied by the proper power of 10, then added together.\nfunc (z *Uint) fromDecimal(bs string) error {\n\t// first clear the input\n\tz.Clear()\n\t// the maximum value of uint64 is 18446744073709551615, which is 20 characters\n\t// one less means that a string of 19 9's is always within the uint64 limit\n\tvar (\n\t\tnum       uint64\n\t\terr       error\n\t\tremaining = len(bs)\n\t)\n\tif remaining == 0 {\n\t\treturn errors.New(\"EOF\")\n\t}\n\t// We proceed in steps of 19 characters (nibbles), from least significant to most significant.\n\t// This means that the first (up to) 19 characters do not need to be multiplied.\n\t// In the second iteration, our slice of 19 characters needs to be multipleied\n\t// by a factor of 10^19. Et cetera.\n\tfor i, mult := range multipliers {\n\t\tif remaining \u003c= 0 {\n\t\t\treturn nil // Done\n\t\t} else if remaining \u003e 19 {\n\t\t\tnum, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64)\n\t\t} else {\n\t\t\t// Final round\n\t\t\tnum, err = strconv.ParseUint(bs, 10, 64)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// add that number to our running total\n\t\tif i == 0 {\n\t\t\tz.SetUint64(num)\n\t\t} else {\n\t\t\tbase := NewUint(num)\n\t\t\tz.Add(z, base.Mul(base, mult))\n\t\t}\n\t\t// Chop off another 19 characters\n\t\tif remaining \u003e 19 {\n\t\t\tbs = bs[0 : remaining-19]\n\t\t}\n\t\tremaining -= 19\n\t}\n\treturn nil\n}\n\n// Byte sets z to the value of the byte at position n,\n// with 'z' considered as a big-endian 32-byte integer\n// if 'n' \u003e 32, f is set to 0\n// Example: f = '5', n=31 =\u003e 5\nfunc (z *Uint) Byte(n *Uint) *Uint {\n\t// in z, z.arr[0] is the least significant\n\tif number, overflow := n.Uint64WithOverflow(); !overflow {\n\t\tif number \u003c 32 {\n\t\t\tnumber := z.arr[4-1-number/8]\n\t\t\toffset := (n.arr[0] \u0026 0x7) \u003c\u003c 3 // 8*(n.d % 8)\n\t\t\tz.arr[0] = (number \u0026 (0xff00000000000000 \u003e\u003e offset)) \u003e\u003e (56 - offset)\n\t\t\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\t\t\treturn z\n\t\t}\n\t}\n\n\treturn z.Clear()\n}\n\n// BitLen returns the number of bits required to represent z\nfunc (z *Uint) BitLen() int {\n\tswitch {\n\tcase z.arr[3] != 0:\n\t\treturn 192 + bits.Len64(z.arr[3])\n\tcase z.arr[2] != 0:\n\t\treturn 128 + bits.Len64(z.arr[2])\n\tcase z.arr[1] != 0:\n\t\treturn 64 + bits.Len64(z.arr[1])\n\tdefault:\n\t\treturn bits.Len64(z.arr[0])\n\t}\n}\n\n// ByteLen returns the number of bytes required to represent z\nfunc (z *Uint) ByteLen() int {\n\treturn (z.BitLen() + 7) / 8\n}\n\n// Clear sets z to 0\nfunc (z *Uint) Clear() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 0\n\treturn z\n}\n\nconst (\n\t// hextable  = \"0123456789abcdef\"\n\tbintable  = \"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\a\\b\\t\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n\tbadNibble = 0xff\n)\n\n// SetFromHex sets z from the given string, interpreted as a hexadecimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Int).SetString(..., 16) method.\n// Notable differences:\n// - This method _require_ \"0x\" or \"0X\" prefix.\n// - This method does not accept zero-prefixed hex, e.g. \"0x0001\"\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0x0\",\n//   - (this method does not accept any negative input as valid)\nfunc (z *Uint) SetFromHex(hex string) error {\n\treturn z.fromHex(hex)\n}\n\n// fromHex is the internal implementation of parsing a hex-string.\nfunc (z *Uint) fromHex(hex string) error {\n\tif err := checkNumberS(hex); err != nil {\n\t\treturn err\n\t}\n\tif len(hex) \u003e 66 {\n\t\treturn ErrBig256Range\n\t}\n\tz.Clear()\n\tend := len(hex)\n\tfor i := 0; i \u003c 4; i++ {\n\t\tstart := end - 16\n\t\tif start \u003c 2 {\n\t\t\tstart = 2\n\t\t}\n\t\tfor ri := start; ri \u003c end; ri++ {\n\t\t\tnib := bintable[hex[ri]]\n\t\t\tif nib == badNibble {\n\t\t\t\treturn ErrSyntax\n\t\t\t}\n\t\t\tz.arr[i] = z.arr[i] \u003c\u003c 4\n\t\t\tz.arr[i] += uint64(nib)\n\t\t}\n\t\tend = start\n\t}\n\treturn nil\n}\n\n// FromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string. The string is required to be '0x'-prefixed\n// Numbers larger than 256 bits are not accepted.\nfunc FromHex(hex string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromHex(hex string) *Uint {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// Clone creates a new Uint identical to z\nfunc (z *Uint) Clone() *Uint {\n\tvar x Uint\n\tx.arr[0] = z.arr[0]\n\tx.arr[1] = z.arr[1]\n\tx.arr[2] = z.arr[2]\n\tx.arr[3] = z.arr[3]\n\n\treturn \u0026x\n}\n"},{"name":"utils.gno","body":"package uint256\n\nfunc checkNumberS(input string) error {\n\tconst fn = \"UnmarshalText\"\n\tl := len(input)\n\tif l == 0 {\n\t\treturn errEmptyString(fn, input)\n\t}\n\tif l \u003c 2 || input[0] != '0' ||\n\t\t(input[1] != 'x' \u0026\u0026 input[1] != 'X') {\n\t\treturn errMissingPrefix(fn, input)\n\t}\n\tif l == 2 {\n\t\treturn errEmptyNumber(fn, input)\n\t}\n\tif len(input) \u003e 3 \u0026\u0026 input[2] == '0' {\n\t\treturn errLeadingZero(fn, input)\n\t}\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"issue5736_common","path":"gno.land/r/tests/issue5736_common","files":[{"name":"common.gno","body":"// Package issue5736_common is a regression fixture for\n// https://github.com/gnolang/gno/issues/5736.\n//\n// It mimics the gnoswap pattern that surfaced the bug: a non-crossing\n// helper in an /r/ realm that performs in-place uint256 arithmetic on\n// a value handed in from another /r/ realm. Before the fix to\n// {Array,Struct}Value.Copy, this triggered:\n//\n//\tpanic: cannot directly modify readonly tainted object\n//\t  gno.land/p/onbloc/uint256/bitwise.gno (z.arr[0] = z.arr[0] \u003c\u003c n)\npackage issue5736_common\n\nimport (\n\tu256 \"gno.land/p/onbloc/uint256\"\n)\n\nfunc DoLsh(x *u256.Uint, n uint) *u256.Uint {\n\treturn u256.Zero().Lsh(x, n)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/issue5736_common\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"issue5736_bar","path":"gno.land/r/tests/issue5736_bar","files":[{"name":"bar.gno","body":"// Package issue5736_bar is the caller-side fixture for\n// https://github.com/gnolang/gno/issues/5736. See the sibling\n// gno.land/r/tests/issue5736_common package for context.\npackage issue5736_bar\n\nimport (\n\tu256 \"gno.land/p/onbloc/uint256\"\n\t\"gno.land/r/tests/issue5736_common\"\n)\n\nfunc Call(cur realm) string {\n\tout := issue5736_common.DoLsh(u256.NewUint(123), 1)\n\treturn out.Dec()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/issue5736_bar\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"feed","path":"gno.land/p/demo/gnorkle/feed","files":[{"name":"errors.gno","body":"package feed\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined feed\")\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/feed\"\ngno = \"0.9\"\n"},{"name":"task.gno","body":"package feed\n\n// Task is a unit of work that can be part of a `Feed` definition. Tasks\n// are executed by agents.\ntype Task interface {\n\tMarshalJSON() ([]byte, error)\n}\n"},{"name":"type.gno","body":"package feed\n\n// Type indicates the type of a feed.\ntype Type int\n\nconst (\n\t// TypeStatic indicates a feed cannot be changed once the first value is committed.\n\tTypeStatic Type = iota\n\t// TypeContinuous indicates a feed can continuously ingest values and will publish\n\t// a new value on request using the values it has ingested.\n\tTypeContinuous\n\t// TypePeriodic indicates a feed can accept one or more values within a certain period\n\t// and will proceed to commit these values at the end up each period to produce an\n\t// aggregate value before starting a new period.\n\tTypePeriodic\n)\n"},{"name":"value.gno","body":"package feed\n\nimport \"time\"\n\n// Value represents a value published by a feed. The `Time` is when the value was published.\ntype Value struct {\n\tString string\n\tTime   time.Time\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"message","path":"gno.land/p/demo/gnorkle/message","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/message\"\ngno = \"0.9\"\n"},{"name":"parse.gno","body":"package message\n\nimport \"strings\"\n\n// ParseFunc parses a raw message and returns the message function\n// type extracted from the remainder of the message.\nfunc ParseFunc(rawMsg string) (FuncType, string) {\n\tfuncType, remainder := parseFirstToken(rawMsg)\n\treturn FuncType(funcType), remainder\n}\n\n// ParseID parses a raw message and returns the ID extracted from\n// the remainder of the message.\nfunc ParseID(rawMsg string) (string, string) {\n\treturn parseFirstToken(rawMsg)\n}\n\nfunc parseFirstToken(rawMsg string) (string, string) {\n\tmsgParts := strings.SplitN(rawMsg, \",\", 2)\n\tif len(msgParts) \u003c 2 {\n\t\treturn msgParts[0], \"\"\n\t}\n\n\treturn msgParts[0], msgParts[1]\n}\n"},{"name":"type.gno","body":"package message\n\n// FuncType is the type of function that is being called by the agent.\ntype FuncType string\n\nconst (\n\t// FuncTypeIngest means the agent is sending data for ingestion.\n\tFuncTypeIngest FuncType = \"ingest\"\n\t// FuncTypeCommit means the agent is requesting a feed commit the transitive data\n\t// being held by its ingester.\n\tFuncTypeCommit FuncType = \"commit\"\n\t// FuncTypeRequest means the agent is requesting feed definitions for all those\n\t// that it is whitelisted to provide data for.\n\tFuncTypeRequest FuncType = \"request\"\n)\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"gnorkle","path":"gno.land/p/demo/gnorkle/gnorkle","files":[{"name":"feed.gno","body":"package gnorkle\n\nimport (\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Feed is an abstraction used by a gnorkle `Instance` to ingest data from\n// agents and provide data feeds to consumers.\ntype Feed interface {\n\tID() string\n\tType() feed.Type\n\tValue() (value feed.Value, dataType string, consumable bool)\n\tIngest(funcType message.FuncType, rawMessage, providerAddress string) error\n\tMarshalJSON() ([]byte, error)\n\tTasks() []feed.Task\n\tIsActive() bool\n}\n\n// FeedWithWhitelist associates a `Whitelist` with a `Feed`.\ntype FeedWithWhitelist struct {\n\tFeed\n\tWhitelist\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/gnorkle\"\ngno = \"0.9\"\n"},{"name":"ingester.gno","body":"package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/ingester\"\n\n// Ingester is the abstraction that allows a `Feed` to ingest data from agents\n// and commit it to storage using zero or more intermediate aggregation steps.\ntype Ingester interface {\n\tType() ingester.Type\n\tIngest(value, providerAddress string) (canAutoCommit bool, err error)\n\tCommitValue(storage Storage, providerAddress string) error\n}\n"},{"name":"instance.gno","body":"package gnorkle\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Instance is a single instance of an oracle.\ntype Instance struct {\n\tfeeds     *avl.Tree\n\twhitelist agent.Whitelist\n}\n\n// NewInstance creates a new instance of an oracle.\nfunc NewInstance() *Instance {\n\treturn \u0026Instance{\n\t\tfeeds: avl.NewTree(),\n\t}\n}\n\nfunc assertValidID(id string) error {\n\tif len(id) == 0 {\n\t\treturn errors.New(\"feed ids cannot be empty\")\n\t}\n\n\tif strings.Contains(id, \",\") {\n\t\treturn errors.New(\"feed ids cannot contain commas\")\n\t}\n\n\treturn nil\n}\n\nfunc (i *Instance) assertFeedDoesNotExist(id string) error {\n\tif i.feeds.Has(id) {\n\t\treturn errors.New(\"feed already exists\")\n\t}\n\n\treturn nil\n}\n\n// AddFeeds adds feeds to the instance with empty whitelists.\nfunc (i *Instance) AddFeeds(feeds ...Feed) error {\n\tfor _, feed := range feeds {\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: new(agent.Whitelist),\n\t\t\t\tFeed:      feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// AddFeedsWithWhitelists adds feeds to the instance with the given whitelists.\nfunc (i *Instance) AddFeedsWithWhitelists(feeds ...FeedWithWhitelist) error {\n\tfor _, feed := range feeds {\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: feed.Whitelist,\n\t\t\t\tFeed:      feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// RemoveFeed removes a feed from the instance.\nfunc (i *Instance) RemoveFeed(id string) {\n\ti.feeds.Remove(id)\n}\n\n// PostMessageHandler is a type that allows for post-processing of feed state after a feed\n// ingests a message from an agent.\ntype PostMessageHandler interface {\n\tHandle(i *Instance, funcType message.FuncType, feed Feed) error\n}\n\n// HandleMessage handles a message from an agent and routes to either the logic that returns\n// feed definitions or the logic that allows a feed to ingest a message.\n//\n// TODO: Consider further message types that could allow administrative action such as modifying\n// a feed's whitelist without the owner of this oracle having to maintain a reference to it.\nfunc (i *Instance) HandleMessage(msg string, postHandler PostMessageHandler) (string, error) {\n\tcaller := string(unsafe.OriginCaller())\n\n\tfuncType, msg := message.ParseFunc(msg)\n\n\tswitch funcType {\n\tcase message.FuncTypeRequest:\n\t\treturn i.GetFeedDefinitions(caller)\n\n\tdefault:\n\t\tid, msg := message.ParseID(msg)\n\t\tif err := assertValidID(id); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tfeedWithWhitelist, err := i.getFeedWithWhitelist(id)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, caller, nil) {\n\t\t\treturn \"\", errors.New(\"caller not whitelisted\")\n\t\t}\n\n\t\tif err := feedWithWhitelist.Ingest(funcType, msg, caller); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif postHandler != nil {\n\t\t\tpostHandler.Handle(i, funcType, feedWithWhitelist)\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (i *Instance) getFeed(id string) (Feed, error) {\n\tuntypedFeed := i.feeds.Get(id)\n\tif untypedFeed == nil {\n\t\treturn nil, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeed, ok := untypedFeed.(Feed)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid feed type\")\n\t}\n\n\treturn feed, nil\n}\n\nfunc (i *Instance) getFeedWithWhitelist(id string) (FeedWithWhitelist, error) {\n\tuntypedFeedWithWhitelist := i.feeds.Get(id)\n\tif untypedFeedWithWhitelist == nil {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeedWithWhitelist, ok := untypedFeedWithWhitelist.(FeedWithWhitelist)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid feed with whitelist type\")\n\t}\n\n\treturn feedWithWhitelist, nil\n}\n\n// GetFeedValue returns the most recently published value of a feed along with a string\n// representation of the value's type and boolean indicating whether the value is\n// okay for consumption.\nfunc (i *Instance) GetFeedValue(id string) (feed.Value, string, bool, error) {\n\tfoundFeed, err := i.getFeed(id)\n\tif err != nil {\n\t\treturn feed.Value{}, \"\", false, err\n\t}\n\n\tvalue, valueType, consumable := foundFeed.Value()\n\treturn value, valueType, consumable, nil\n}\n\n// GetFeedDefinitions returns a JSON string representing the feed definitions for which the given\n// agent address is whitelisted to provide values for ingestion.\nfunc (i *Instance) GetFeedDefinitions(forAddress string) (string, error) {\n\tinstanceHasAddressWhitelisted := !i.whitelist.HasDefinition() || i.whitelist.HasAddress(forAddress)\n\n\tbuf := new(strings.Builder)\n\tbuf.WriteString(\"[\")\n\tfirst := true\n\tvar err error\n\n\t// The boolean value returned by this callback function indicates whether to stop iterating.\n\ti.feeds.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tfeedWithWhitelist, ok := value.(FeedWithWhitelist)\n\t\tif !ok {\n\t\t\terr = errors.New(\"invalid feed type\")\n\t\t\treturn true\n\t\t}\n\n\t\t// Don't give agents the ability to try to publish to inactive feeds.\n\t\tif !feedWithWhitelist.IsActive() {\n\t\t\treturn false\n\t\t}\n\n\t\t// Skip feeds the address is not whitelisted for.\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, forAddress, \u0026instanceHasAddressWhitelisted) {\n\t\t\treturn false\n\t\t}\n\n\t\tvar taskBytes []byte\n\t\tif taskBytes, err = feedWithWhitelist.Feed.MarshalJSON(); err != nil {\n\t\t\treturn true\n\t\t}\n\n\t\t// Guard against any tasks that shouldn't be returned; maybe they are not active because they have\n\t\t// already been completed.\n\t\tif len(taskBytes) == 0 {\n\t\t\treturn false\n\t\t}\n\n\t\tif !first {\n\t\t\tbuf.WriteString(\",\")\n\t\t}\n\n\t\tfirst = false\n\t\tbuf.Write(taskBytes)\n\t\treturn false\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf.WriteString(\"]\")\n\treturn buf.String(), nil\n}\n"},{"name":"storage.gno","body":"package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/feed\"\n\n// Storage defines how published feed values should be read\n// and written.\ntype Storage interface {\n\tPut(value string) error\n\tGetLatest() feed.Value\n\tGetHistory() []feed.Value\n}\n"},{"name":"whitelist.gno","body":"package gnorkle\n\n// Whitelist is used to manage which agents are allowed to interact.\ntype Whitelist interface {\n\tClearAddresses()\n\tAddAddresses(addresses []string)\n\tRemoveAddress(address_XXX string)\n\tHasDefinition() bool\n\tHasAddress(address_XXX string) bool\n}\n\n// ClearWhitelist clears the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) ClearWhitelist(feedID string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.ClearAddresses()\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.ClearAddresses()\n\treturn nil\n}\n\n// AddToWhitelist adds the given addresses to the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) AddToWhitelist(feedID string, addresses []string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.AddAddresses(addresses)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.AddAddresses(addresses)\n\treturn nil\n}\n\n// RemoveFromWhitelist removes the given address from the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) RemoveFromWhitelist(feedID string, address_XXX string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.RemoveAddress(address_XXX)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.RemoveAddress(address_XXX)\n\treturn nil\n}\n\n// addressWhiteListed returns true if:\n// - the feed has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has no white list.\nfunc addressIsWhitelisted(instanceWhitelist, feedWhitelist Whitelist, address_XXX string, instanceWhitelistedOverride *bool) bool {\n\t// A feed whitelist takes priority, so it will return false if the feed has a whitelist and the caller is\n\t// not a part of it. An empty whitelist defers to the instance whitelist.\n\tif feedWhitelist != nil {\n\t\tif feedWhitelist.HasDefinition() \u0026\u0026 !feedWhitelist.HasAddress(address_XXX) {\n\t\t\treturn false\n\t\t}\n\n\t\t// Getting to this point means that one of the following is true:\n\t\t// - the feed has no defined whitelist (so it can't possibly have the address whitelisted)\n\t\t// - the feed has a defined whitelist and the caller is a part of it\n\t\t//\n\t\t// In this case, we can be sure that the boolean indicating whether the feed has this address whitelisted\n\t\t// is equivalent to the boolean indicating whether the feed has a defined whitelist.\n\t\tif feedWhitelist.HasDefinition() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif instanceWhitelistedOverride != nil {\n\t\treturn *instanceWhitelistedOverride\n\t}\n\n\t// We were unable able to determine whether this address is allowed after looking at the feed whitelist,\n\t// so fall back to the instance whitelist. A complete absence of values in the instance whitelist means\n\t// that the instance has no whitelist so we can return true because everything is allowed by default.\n\tif instanceWhitelist == nil || !instanceWhitelist.HasDefinition() {\n\t\treturn true\n\t}\n\n\t// The instance whitelist is defined so if the address is present then it is allowed.\n\treturn instanceWhitelist.HasAddress(address_XXX)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"single","path":"gno.land/p/demo/gnorkle/ingesters/single","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/ingesters/single\"\ngno = \"0.9\"\n"},{"name":"ingester.gno","body":"package single\n\nimport (\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n)\n\n// ValueIngester is an ingester that ingests a single value.\ntype ValueIngester struct {\n\tvalue string\n}\n\n// Type returns the type of the ingester.\nfunc (i *ValueIngester) Type() ingester.Type {\n\treturn ingester.TypeSingle\n}\n\n// Ingest ingests a value provided by the given agent address.\nfunc (i *ValueIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i == nil {\n\t\treturn false, ingester.ErrUndefined\n\t}\n\n\ti.value = value\n\treturn true, nil\n}\n\n// CommitValue commits the ingested value to the given storage instance.\nfunc (i *ValueIngester) CommitValue(valueStorer gnorkle.Storage, providerAddress string) error {\n\tif i == nil {\n\t\treturn ingester.ErrUndefined\n\t}\n\n\treturn valueStorer.Put(i.value)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"storage","path":"gno.land/p/demo/gnorkle/storage","files":[{"name":"errors.gno","body":"package storage\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined storage\")\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/storage\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"simple","path":"gno.land/p/demo/gnorkle/storage/simple","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/storage/simple\"\ngno = \"0.9\"\n"},{"name":"storage.gno","body":"package simple\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/storage\"\n)\n\n// Storage is simple, bounded storage for published feed values.\ntype Storage struct {\n\tvalues    []feed.Value\n\tmaxValues uint\n}\n\n// NewStorage creates a new Storage with the given maximum number of values.\n// If maxValues is 0, the storage is bounded to a size of one. If this is not desirable,\n// then don't provide a value of 0.\nfunc NewStorage(maxValues uint) *Storage {\n\tif maxValues == 0 {\n\t\tmaxValues = 1\n\t}\n\n\treturn \u0026Storage{\n\t\tmaxValues: maxValues,\n\t}\n}\n\n// Put adds a new value to the storage. If the storage is full, the oldest value\n// is removed. If maxValues is 0, the storage is bounded to a size of one.\nfunc (s *Storage) Put(value string) error {\n\tif s == nil {\n\t\treturn storage.ErrUndefined\n\t}\n\n\ts.values = append(s.values, feed.Value{String: value, Time: time.Now()})\n\tif uint(len(s.values)) \u003e s.maxValues {\n\t\ts.values = s.values[1:]\n\t}\n\n\treturn nil\n}\n\n// GetLatest returns the most recently added value, or an empty value if none exist.\nfunc (s Storage) GetLatest() feed.Value {\n\tif len(s.values) == 0 {\n\t\treturn feed.Value{}\n\t}\n\n\treturn s.values[len(s.values)-1]\n}\n\n// GetHistory returns all values in the storage, from oldest to newest.\nfunc (s Storage) GetHistory() []feed.Value {\n\treturn s.values\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"static","path":"gno.land/p/demo/gnorkle/feeds/static","files":[{"name":"feed.gno","body":"package static\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Feed is a static feed.\ntype Feed struct {\n\tid            string\n\tisLocked      bool\n\tvalueDataType string\n\tingester      gnorkle.Ingester\n\tstorage       gnorkle.Storage\n\ttasks         []feed.Task\n}\n\n// NewFeed creates a new static feed.\nfunc NewFeed(\n\tid string,\n\tvalueDataType string,\n\tingester gnorkle.Ingester,\n\tstorage gnorkle.Storage,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn \u0026Feed{\n\t\tid:            id,\n\t\tvalueDataType: valueDataType,\n\t\tingester:      ingester,\n\t\tstorage:       storage,\n\t\ttasks:         tasks,\n\t}\n}\n\n// NewSingleValueFeed is a convenience function  for creating a static feed\n// that autocommits a value after a single ingestion.\nfunc NewSingleValueFeed(\n\tid string,\n\tvalueDataType string,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn NewFeed(\n\t\tid,\n\t\tvalueDataType,\n\t\t\u0026single.ValueIngester{},\n\t\tsimple.NewStorage(1),\n\t\ttasks...,\n\t)\n}\n\n// ID returns the feed's ID.\nfunc (f Feed) ID() string {\n\treturn f.id\n}\n\n// Type returns the feed's type.\nfunc (f Feed) Type() feed.Type {\n\treturn feed.TypeStatic\n}\n\n// Ingest ingests a message into the feed. It either adds the value to the ingester's\n// pending values or commits the value to the storage.\nfunc (f *Feed) Ingest(funcType message.FuncType, msg, providerAddress string) error {\n\tif f == nil {\n\t\treturn feed.ErrUndefined\n\t}\n\n\tif f.isLocked {\n\t\treturn errors.New(\"feed locked\")\n\t}\n\n\tswitch funcType {\n\tcase message.FuncTypeIngest:\n\t\t// Autocommit the ingester's value if it's a single value ingester\n\t\t// because this is a static feed and this is the only value it will ever have.\n\t\tif canAutoCommit, err := f.ingester.Ingest(msg, providerAddress); canAutoCommit \u0026\u0026 err == nil {\n\t\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tf.isLocked = true\n\t\t} else if err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase message.FuncTypeCommit:\n\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tf.isLocked = true\n\n\tdefault:\n\t\treturn errors.New(\"invalid message function \" + string(funcType))\n\t}\n\n\treturn nil\n}\n\n// Value returns the feed's latest value, it's data type, and whether or not it can\n// be safely consumed. In this case it uses `f.isLocked` because, this being a static\n// feed, it will only ever have one value; once that value is committed the feed is locked\n// and there is a valid, non-empty value to consume.\nfunc (f Feed) Value() (feed.Value, string, bool) {\n\treturn f.storage.GetLatest(), f.valueDataType, f.isLocked\n}\n\n// MarshalJSON marshals the components of the feed that are needed for\n// an agent to execute tasks and send values for ingestion.\nfunc (f Feed) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write([]byte(\n\t\t`{\"id\":\"` + f.id +\n\t\t\t`\",\"type\":\"` + ufmt.Sprintf(\"%d\", int(f.Type())) +\n\t\t\t`\",\"value_type\":\"` + f.valueDataType +\n\t\t\t`\",\"tasks\":[`),\n\t)\n\n\tfirst := true\n\tfor _, task := range f.tasks {\n\t\tif !first {\n\t\t\tw.WriteString(\",\")\n\t\t}\n\n\t\ttaskJSON, err := task.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.Write(taskJSON)\n\t\tfirst = false\n\t}\n\n\tw.Write([]byte(\"]}\"))\n\tw.Flush()\n\n\treturn buf.Bytes(), nil\n}\n\n// Tasks returns the feed's tasks. This allows task consumers to extract task\n// contents without having to marshal the entire feed.\nfunc (f Feed) Tasks() []feed.Task {\n\treturn f.tasks\n}\n\n// IsActive returns true if the feed is accepting ingestion requests from agents.\nfunc (f Feed) IsActive() bool {\n\treturn !f.isLocked\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/gnorkle/feeds/static\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"testutils","path":"gno.land/p/nt/testutils/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `testutils` - misc testing helpers\n\nSmall grab-bag of helpers for `_test.gno` files: deterministic fake addresses, a call-stack wrapper, and fixtures exercising access rules (exported/unexported fields, methods, and interfaces).\n\n## Usage\n\n```go\nimport (\n    \"testing\"\n\n    \"gno.land/p/nt/testutils/v0\"\n)\n\nfunc TestTransfer(t *testing.T) {\n    alice := testutils.TestAddress(\"alice\") // deterministic g1... address\n    bob := testutils.TestAddress(\"bob\")\n\n    testutils.WrapCall(func() {\n        Transfer(alice, bob, 100)\n    })\n}\n```\n\n## API\n\nAddresses:\n\n```go\n// TestAddress returns a deterministic bech32 g1... address derived from name.\n// name must be at most 20 bytes; it is right-padded with '_' before encoding.\nfunc TestAddress(name string) address\n```\n\nCall-stack helper:\n\n```go\n// WrapCall invokes fn after adding one extra frame to the call stack.\n// Useful for tests that inspect caller depth.\nfunc WrapCall(fn func())\n```\n\nAccess-rule fixtures (used by VM file tests to exercise exported vs. unexported visibility):\n\n```go\ntype TestAccessStruct struct {\n    PublicField  string\n    // privateField is unexported on purpose.\n}\n\nfunc NewTestAccessStruct(pub, priv string) TestAccessStruct\nfunc (TestAccessStruct) PublicMethod() string\n\ntype PrivateInterface interface {\n    // unexported method — only satisfiable from within this package.\n}\n\nfunc PrintPrivateInterface(pi PrivateInterface)\n\nvar TestVar1 int // initialized to 123 in init()\n```\n\n## Notes\n\n- `TestAddress` panics if `name` exceeds 20 bytes; the resulting address is reproducible across runs, which is what you want in tests.\n- The access fixtures exist mainly to back GnoVM file tests under `gnovm/tests/files/`; most user code only needs `TestAddress`.\n"},{"name":"access.gno","body":"package testutils\n\n// for testing access. see tests/files/access*.go\n\n// NOTE: non-package variables cannot be overridden, except during init().\nvar (\n\tTestVar1 int\n\ttestVar2 int\n)\n\nfunc init() {\n\tTestVar1 = 123\n\ttestVar2 = 456\n}\n\ntype TestAccessStruct struct {\n\tPublicField  string\n\tprivateField string\n}\n\nfunc (tas TestAccessStruct) PublicMethod() string {\n\treturn tas.PublicField + \"/\" + tas.privateField\n}\n\nfunc (tas TestAccessStruct) privateMethod() string {\n\treturn tas.PublicField + \"/\" + tas.privateField\n}\n\nfunc NewTestAccessStruct(pub, priv string) TestAccessStruct {\n\treturn TestAccessStruct{\n\t\tPublicField:  pub,\n\t\tprivateField: priv,\n\t}\n}\n\n// see access6.g0 etc.\ntype PrivateInterface interface {\n\tprivateMethod() string\n}\n\nfunc PrintPrivateInterface(pi PrivateInterface) {\n\tprintln(\"testutils.PrintPrivateInterface\", pi.privateMethod())\n}\n"},{"name":"crypto.gno","body":"package testutils\n\nimport \"crypto/bech32\"\n\nfunc TestAddress(name string) address {\n\tif len(name) \u003e 20 {\n\t\tpanic(\"address name cannot be greater than 20 bytes\")\n\t}\n\taddr := []byte(\"____________________\")\n\tcopy(addr[:], name)\n\tconverted, err := bech32.ConvertBits(addr, 8, 5, true)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tenc, err := bech32.Encode(\"g\", converted)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn address(enc)\n}\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package testutils provides testing utilities for Gno packages and realms.\npackage testutils\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/testutils/v0\"\ngno = \"0.9\"\n"},{"name":"misc.gno","body":"package testutils\n\n// WrapCall adds a frame to the call stack, for testing call stack depth.\nfunc WrapCall(fn func()) {\n\tfn()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"combinederr","path":"gno.land/p/nt/combinederr/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `combinederr` - Aggregate multiple errors\n\nCollect several errors into a single `error` value with a semicolon-separated message. Useful for batch operations where you want to report every failure instead of bailing on the first.\n\n## Usage\n\n```go\nimport \"gno.land/p/nt/combinederr/v0\"\n\nce := \u0026combinederr.CombinedError{}\nce.Add(validateName(name))   // nil is silently skipped\nce.Add(validateEmail(email))\nce.Add(validateAge(age))\n\nif ce.Size() \u003e 0 {\n    return ce // \"invalid name; bad email; age must be \u003e 0\"\n}\nreturn nil\n```\n\n## API\n\n```go\n// CombinedError aggregates multiple errors into a single error value.\ntype CombinedError struct { /* ... */ }\n\n// Add appends err to the combined error. Nil errors are ignored.\nfunc (e *CombinedError) Add(err error)\n\n// Error returns all collected errors joined with \"; \".\nfunc (e *CombinedError) Error() string\n\n// Size returns the number of collected errors.\nfunc (e *CombinedError) Size() int\n```\n\n## Notes\n\n- A zero-value `CombinedError{}` with no errors added has `Size() == 0` and `Error() == \"\"`.\n- The pointer receiver on `Add` means you must use `\u0026CombinedError{}`.\n- Aggregation is message-only: there is no `Unwrap`, so `errors.Is`/`errors.As` never see the collected errors. Use it when you want one human-readable combined string, not typed error matching.\n"},{"name":"combinederr.gno","body":"package combinederr\n\nimport \"strings\"\n\n// CombinedError is a combined execution error\ntype CombinedError struct {\n\terrors []error\n}\n\n// Error returns the combined execution error\nfunc (e *CombinedError) Error() string {\n\tif len(e.errors) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tfor _, err := range e.errors {\n\t\tsb.WriteString(err.Error() + \"; \")\n\t}\n\n\t// Remove the last semicolon and space\n\tresult := sb.String()\n\n\treturn result[:len(result)-2]\n}\n\n// Add adds a new error to the execution error\nfunc (e *CombinedError) Add(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\te.errors = append(e.errors, err)\n}\n\n// Size returns a\nfunc (e *CombinedError) Size() int {\n\treturn len(e.errors)\n}\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package combinederr provides a combined error type for aggregating multiple\n// errors into a single error value.\npackage combinederr\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/combinederr/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"params","path":"gno.land/r/sys/params","files":[{"name":"fee_collector.gno","body":"package params\n\nimport (\n\t\"gno.land/r/gov/dao\"\n)\n\nfunc NewSetFeeCollectorRequest(cur realm, addr address) dao.ProposalRequest {\n\treturn NewSysParamStringPropRequest(cur,\n\t\t\"auth\", \"p\", \"fee_collector\",\n\t\taddr.String(),\n\t)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/params\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"halt.gno","body":"package params\n\nimport (\n\t\"strconv\"\n\n\t\"chain\"\n\tprms \"sys/params\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\nconst (\n\tnodeModulePrefix  = \"node\"\n\thaltHeightKey     = \"halt_height\"\n\thaltMinVersionKey = \"halt_min_version\"\n)\n\n// NewSetHaltRequest creates a GovDAO proposal to halt all chain nodes at the given block height.\n// Once approved and executed, nodes will gracefully stop after committing the specified block,\n// enabling coordinated chain upgrades.\n//\n// minVersion, if non-empty, sets the minimum binary version required to resume after the halt.\n// Nodes will refuse to restart unless their version satisfies the minimum requirement,\n// preventing old binaries from accidentally resuming a chain halted for an upgrade.\n// Example: minVersion=\"chain/gnoland1.1\" prevents gnoland1.0 from resuming.\n//\n// Use height=0 to cancel a previously scheduled halt.\nfunc NewSetHaltRequest(cur realm, height int64, minVersion string) dao.ProposalRequest {\n\tcallback := func(cur realm) error {\n\t\tprms.SetSysParamInt64(nodeModulePrefix, \"p\", haltHeightKey, height)\n\t\tprms.SetSysParamString(nodeModulePrefix, \"p\", haltMinVersionKey, minVersion)\n\t\tchain.Emit(\"set_halt\",\n\t\t\t\"height\", strconv.FormatInt(height, 10),\n\t\t\t\"min_version\", minVersion,\n\t\t)\n\t\treturn nil\n\t}\n\n\tvar desc string\n\tif height == 0 {\n\t\tdesc = \"Cancel the scheduled chain halt and clear the minimum version requirement.\"\n\t} else {\n\t\tdesc = \"Halt the chain at block \" + strconv.FormatInt(height, 10) + \".\"\n\t\tif minVersion != \"\" {\n\t\t\tdesc += \" Requires binary version \u003e= \" + minVersion + \" to resume.\"\n\t\t}\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, callback, \"\")\n\treturn dao.NewProposalRequest(\"Set node halt height\", desc, e)\n}\n"},{"name":"params.gno","body":"// Package params provides functions for creating parameter executors that\n// interface with the Params Keeper.\n//\n// This package enables setting various parameter types (such as strings,\n// integers, booleans, and byte slices) through the GovDAO proposal mechanism.\n// Each function returns an executor that, when called, sets the specified\n// parameter in the Params Keeper.\n//\n// The executors are designed to be used within governance proposals to modify\n// parameters dynamically. The integration with the GovDAO allows for parameter\n// changes to be proposed and executed in a controlled manner, ensuring that\n// modifications are subject to governance processes.\n//\n// Example usage:\n//\n//\t// This executor can be used in a governance proposal to set the parameter.\n//\tpr := params.NewSysParamStringPropExecutor(\"bank\", \"p\", \"restricted_denoms\")\npackage params\n\nimport (\n\t\"chain\"\n\tprms \"sys/params\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\n// this is only used for emitting events.\nfunc syskey(module, submodule, name string) string {\n\treturn module + \":\" + submodule + \":\" + name\n}\n\n// assertNotValsetKey rejects governance proposals that target the\n// node:valset:* key family. Those keys are reserved for the realm-side\n// gate in r/sys/params/valset.gno (SetValsetProposal) which checks\n// the immediate caller is gno.land/r/sys/validators/v3. Without this\n// guard, a generic NewSysParam*PropRequest(\"node\",\"valset\",...) would\n// let any GovDAO supermajority bypass the v3 authorization and write\n// validator-set state directly.\nfunc assertNotValsetKey(module, submodule string) {\n\tif module == \"node\" \u0026\u0026 submodule == \"valset\" {\n\t\tpanic(\"node:valset:* is reserved for r/sys/validators/v3; use it instead of the generic factory\")\n\t}\n}\n\nfunc NewSysParamStringPropRequest(cur realm, module, submodule, name, value string) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamString(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamInt64PropRequest(cur realm, module, submodule, name string, value int64) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamInt64(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamUint64PropRequest(cur realm, module, submodule, name string, value uint64) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamUint64(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamBoolPropRequest(cur realm, module, submodule, name string, value bool) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamBool(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamBytesPropRequest(cur realm, module, submodule, name string, value []byte) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamBytes(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamStringsPropRequest(cur realm, module, submodule, name string, value []string) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamStrings(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamStringsPropRequestWithTitle(cur realm, module, submodule, name, title string, value []string) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamStrings(module, submodule, name, value) },\n\t\ttitle,\n\t)\n}\nfunc NewSysParamStringsPropRequestAddWithTitle(cur realm, module, submodule, name, title string, value []string) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.UpdateSysParamStrings(module, submodule, name, value, true) },\n\t\ttitle,\n\t)\n}\nfunc NewSysParamStringsPropRequestRemoveWithTitle(cur realm, module, submodule, name, title string, value []string) dao.ProposalRequest {\n\tassertNotValsetKey(module, submodule)\n\treturn newPropRequest(cur,\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.UpdateSysParamStrings(module, submodule, name, value, false) },\n\t\ttitle,\n\t)\n}\nfunc newPropRequest(cur realm, key string, fn func(), title string) dao.ProposalRequest {\n\tcallback := func(cur realm) error {\n\t\tfn()\n\t\tchain.Emit(\"set\", \"key\", key) // TODO document, make const, make consistent. 'k'??\n\t\treturn nil\n\t}\n\n\tif title == \"\" {\n\t\ttitle = \"Set new sys/params key\"\n\t}\n\n\te := dao.NewSimpleExecutor(0, cur, callback, \"\")\n\n\treturn dao.NewProposalRequest(title, \"This proposal wants to add a new key to sys/params: \"+key, e)\n}\n"},{"name":"unlock.gno","body":"package params\n\nimport \"gno.land/r/gov/dao\"\n\nconst (\n\tbankModulePrefix     = \"bank\"\n\trestrictedDenomsKey  = \"restricted_denoms\"\n\tunlockTransferTitle  = \"Proposal to unlock the transfer of ugnot.\"\n\tlockTransferTitle    = \"Proposal to lock the transfer of ugnot.\"\n\tauthModulePrefix     = \"auth\"\n\tunrestrictedAddrsKey = \"unrestricted_addrs\"\n)\n\nfunc ProposeUnlockTransferRequest(cur realm) dao.ProposalRequest {\n\treturn NewSysParamStringsPropRequestWithTitle(cur, bankModulePrefix, \"p\", restrictedDenomsKey, unlockTransferTitle, []string{})\n}\n\nfunc ProposeLockTransferRequest(cur realm) dao.ProposalRequest {\n\treturn NewSysParamStringsPropRequestWithTitle(cur, bankModulePrefix, \"p\", restrictedDenomsKey, lockTransferTitle, []string{\"ugnot\"})\n}\n\nfunc ProposeAddUnrestrictedAcctsRequest(cur realm, addrs ...address) dao.ProposalRequest {\n\taddrStrings := make([]string, 0, len(addrs))\n\tfor _, addr := range addrs {\n\t\ts := addr.String()\n\t\taddrStrings = append(addrStrings, s)\n\t}\n\treturn NewSysParamStringsPropRequestAddWithTitle(cur, authModulePrefix, \"p\", unrestrictedAddrsKey, \"Add unrestricted transfer accounts\", addrStrings)\n}\n\nfunc ProposeRemoveUnrestrictedAcctsRequest(cur realm, addrs ...address) dao.ProposalRequest {\n\taddrStrings := make([]string, 0, len(addrs))\n\tfor _, addr := range addrs {\n\t\ts := addr.String()\n\t\taddrStrings = append(addrStrings, s)\n\t}\n\treturn NewSysParamStringsPropRequestRemoveWithTitle(cur, authModulePrefix, \"p\", unrestrictedAddrsKey, \"Add unrestricted transfer accounts\", addrStrings)\n}\n"},{"name":"valoper.gno","body":"package params\n\nimport (\n\tprms \"sys/params\"\n)\n\n// Valoper sys-param keys consumed by r/gnops/valopers (Register +\n// UpdateSigningKey). Governance can update them via the generic\n// NewSysParam*PropRequest factories — no realm-side gate is needed\n// because the values only affect fees and throttle inside valopers.\nconst (\n\tvaloperSubmodule = \"valoper\"\n\n\tvaloperRegisterFeeKey          = \"register_fee\"\n\tvaloperRotationFeeKey          = \"rotation_fee\"\n\tvaloperRotationPeriodBlocksKey = \"rotation_period_blocks\"\n)\n\n// Default values used when the sys-param has never been set by\n// governance. Zero fees (GNOT transfers are disabled chain-wide\n// pre-fork) and a ~1-hour throttle at 6s/block.\nconst (\n\tdefaultValoperRegisterFee          = uint64(0)\n\tdefaultValoperRotationFee          = uint64(0)\n\tdefaultValoperRotationPeriodBlocks = int64(600)\n)\n\n// GetValoperRegisterFee returns the fee (in ugnot) required to call\n// valopers.Register. Defaults to 0 if governance hasn't set it.\nfunc GetValoperRegisterFee() uint64 {\n\tv, ok := prms.GetSysParamUint64(nodeModulePrefix, valoperSubmodule, valoperRegisterFeeKey)\n\tif !ok {\n\t\treturn defaultValoperRegisterFee\n\t}\n\treturn v\n}\n\n// GetValoperRotationFee returns the fee (in ugnot) required to call\n// valopers.UpdateSigningKey. Defaults to 0.\nfunc GetValoperRotationFee() uint64 {\n\tv, ok := prms.GetSysParamUint64(nodeModulePrefix, valoperSubmodule, valoperRotationFeeKey)\n\tif !ok {\n\t\treturn defaultValoperRotationFee\n\t}\n\treturn v\n}\n\n// GetValoperRotationPeriodBlocks returns the per-operator rotation\n// throttle (in blocks). Defaults to ~1h worth at 6s/block (600).\n// This is the primary anti-spam defense pre-fee while rotation_fee\n// stays at 0; tightens further once non-zero fees become enforceable.\nfunc GetValoperRotationPeriodBlocks() int64 {\n\tv, ok := prms.GetSysParamInt64(nodeModulePrefix, valoperSubmodule, valoperRotationPeriodBlocksKey)\n\tif !ok {\n\t\treturn defaultValoperRotationPeriodBlocks\n\t}\n\treturn v\n}\n"},{"name":"valset.gno","body":"package params\n\nimport (\n\t\"chain\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\tprms \"sys/params\"\n\n\t\"gno.land/p/sys/validators\"\n)\n\n// Param keys read by gno.land/pkg/gnoland (EndBlocker).\n// Keep in sync with gno.land/pkg/gnoland/node_params.go.\n// nodeModulePrefix is declared in halt.gno (same package).\nconst (\n\tvalsetSubmodule = \"valset\"\n\n\t// dirty signals the chain that the proposed valset differs from the\n\t// current applied one. Realm sets true; EndBlocker clears.\n\tvalsetDirtyKey = \"dirty\"\n\n\t// One []string per slot; each entry has the form \"\u003cpubkey\u003e:\u003cpower\u003e\"\n\t// (bech32 pubkey + decimal power). Address is derived from pubkey\n\t// on the chain side and not stored.\n\t//\n\t//   proposed = v3's full target valset\n\t//   current  = chain-managed: the set that becomes ACTIVE AT H+2\n\t//              once the most recent EndBlock's updates apply.\n\t//              NOT necessarily the set actively signing the current\n\t//              block — see ABCI H+2 sequencing.\n\tvalsetProposedKey = \"proposed\"\n\tvalsetCurrentKey  = \"current\"\n\n\t// pubkey_types: chain-mirrored validator pubkey-type allow-list (read-only here).\n\tvalsetPubKeyTypesKey = \"pubkey_types\"\n\n\t// Only this realm may write valset:proposed and valset:dirty.\n\t// valset:current is chain-managed (see ctx-sentinel in node_params.go).\n\tvalsetAuthorizedRealm = \"gno.land/r/sys/validators/v3\"\n)\n\n// SetValsetProposal publishes the realm's desired valset. Each entry is\n// \"\u003cbech32-pubkey\u003e:\u003cdecimal-power\u003e\"; power=0 removes the validator.\n// The chain reads this on the next EndBlocker, diffs it against\n// valset:current, and propagates the changes to consensus.\nfunc SetValsetProposal(cur realm, entries []string) {\n\tassertValsetCaller(0, cur)\n\tprms.SetSysParamStrings(nodeModulePrefix, valsetSubmodule, valsetProposedKey, entries)\n\tprms.SetSysParamBool(nodeModulePrefix, valsetSubmodule, valsetDirtyKey, true)\n}\n\n// GetValsetEntries returns the chain's authoritative committed\n// validator set (the contents of valset:current). This is the\n// V_{H+2} view — the set that will be active at H+2 once the most\n// recent EndBlock's updates apply, NOT the set signing the current\n// block. Callers that want \"what v3 reports as the current\n// validator set\" — including the in-flight proposed set during\n// the dirty window — should call GetValsetEffective instead.\nfunc GetValsetEntries() []validators.Validator {\n\treturn parseValsetSlot(valsetCurrentKey)\n}\n\n// ValsetDirty reports whether valset:proposed is awaiting EndBlocker.\n// Realm callers MUST treat this as transient: the dirty flag is set\n// by SetValsetProposal and cleared by the chain's EndBlocker (every\n// block where dirty=true on entry exits with dirty=false).\nfunc ValsetDirty() bool {\n\td, _ := prms.GetSysParamBool(nodeModulePrefix, valsetSubmodule, valsetDirtyKey)\n\treturn d\n}\n\n// GetValsetEffective returns the set that WILL be active at H+2:\n// valset:proposed if dirty, else valset:current. Used by v3 so that\n// (a) reads after a same-block proposal callback see that proposal's\n// effects, and (b) sequential same-block proposals accumulate\n// correctly on top of each other.\n//\n// Misuse warning: this exists for r/sys/validators/v3's internal\n// reads. Other realms making \"is X a validator\" decisions should\n// call v3.IsValidator, not this directly, so future changes to v3's\n// read semantics propagate uniformly.\nfunc GetValsetEffective() []validators.Validator {\n\tkey := valsetCurrentKey\n\tif ValsetDirty() {\n\t\tkey = valsetProposedKey\n\t}\n\treturn parseValsetSlot(key)\n}\n\nfunc parseValsetSlot(key string) []validators.Validator {\n\traw, _ := prms.GetSysParamStrings(nodeModulePrefix, valsetSubmodule, key)\n\tout := make([]validators.Validator, 0, len(raw))\n\tfor _, e := range raw {\n\t\tv, err := parseEntry(e)\n\t\tif err != nil {\n\t\t\tpanic(\"valset:\" + key + \" corrupted: \" + err.Error())\n\t\t}\n\t\tout = append(out, v)\n\t}\n\treturn out\n}\n\n// parseEntry splits \"\u003cbech32-pubkey\u003e:\u003cdecimal-power\u003e\" and derives the\n// validator address via the chain.PubKeyAddress native helper.\nfunc parseEntry(entry string) (validators.Validator, error) {\n\tpkStr, pStr, ok := strings.Cut(entry, \":\")\n\tif !ok {\n\t\treturn validators.Validator{}, errors.New(\"missing ':' separator in \" + entry)\n\t}\n\taddr, err := chain.PubKeyAddress(pkStr)\n\tif err != nil {\n\t\treturn validators.Validator{}, err\n\t}\n\tpower, err := strconv.ParseUint(pStr, 10, 64)\n\tif err != nil {\n\t\treturn validators.Validator{}, err\n\t}\n\treturn validators.Validator{\n\t\tAddress:     addr,\n\t\tPubKey:      pkStr,\n\t\tVotingPower: power,\n\t}, nil\n}\n\n// GetValsetPubKeyTypes returns the validator pubkey-type allow-list mirrored from consensus params (empty means accept any).\nfunc GetValsetPubKeyTypes() []string {\n\ttypes, _ := prms.GetSysParamStrings(nodeModulePrefix, valsetSubmodule, valsetPubKeyTypesKey)\n\treturn types\n}\n\nfunc assertValsetCaller(_ int, rlm realm) {\n\t// Defense-in-depth IsCurrent gate. Current call sites all pass live\n\t// cur (cross(cur) from cache.gno / proposal.gno), so this never\n\t// fires today — but the helper trusts its rlm input, and a future\n\t// caller that threads a stashed/sibling-frame realm value would\n\t// silently bypass the PkgPath check below (Class-2 designation\n\t// forgery; see docs/resources/gno-security.md). Gating here makes\n\t// the precondition enforceable rather than convention.\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tif rlm.Previous().PkgPath() != valsetAuthorizedRealm {\n\t\tpanic(\"unauthorized: only \" + valsetAuthorizedRealm + \" may write valset params\")\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"validators","path":"gno.land/r/sys/validators/v3","files":[{"name":"cache.gno","body":"package validators\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"sort\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/sys/validators\"\n\tsysparams \"gno.land/r/sys/params\"\n)\n\n// valopersRealmPath is the only realm allowed to refresh valoperCache\n// or invoke RotateValoperSigningKey. Both auth checks below depend on\n// being called via `cross` from a crossing function in valopers.\nconst valopersRealmPath = \"gno.land/r/gnops/valopers\"\n\n// valoperCache mirrors the (operator -\u003e current signing key) view from\n// r/gnops/valopers. Written by valopers via NotifyValoperChanged. Read\n// by future operator-keyed proposal flow (step 5).\n//\n// Pushing (valopers passes the values as args) rather than pulling\n// (v3 imports valopers and reads them) — pulling would create an\n// import cycle, since valopers already imports v3 for IsValidator.\nvar valoperCache = bptree.NewBPTree32()\n\ntype cacheEntry struct {\n\tSigningPubKey  string\n\tSigningAddress address\n\tKeepRunning    bool\n}\n\n// assertValopersCaller panics if the caller realm is not r/gnops/valopers.\n// Per docs/resources/gno-interrealm.md, this check works only when (a)\n// the host function is a crossing function (`cur realm`) and (b) it's\n// invoked via `cross` from a crossing function in valopers. Then\n// PreviousRealm() shifts exactly one frame to valopers. A user MsgCall\n// would see PreviousRealm() == UserRealm (pkgpath \"\"); a third realm\n// cross-call would see its own pkgpath. Either fails this check.\nfunc assertValopersCaller(_ int, rlm realm) {\n\t// Defense-in-depth IsCurrent gate. Current call sites pass live\n\t// cur (cross(cur) from valopers), so this never fires today — but\n\t// the helper trusts its rlm input, and any future call that threads\n\t// a stashed/sibling-frame realm value with .Previous().PkgPath() ==\n\t// valopersRealmPath would silently bypass the gate (Class-2\n\t// designation forgery; see docs/resources/gno-security.md). Gating\n\t// here makes the precondition enforceable rather than convention.\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tcaller := rlm.Previous().PkgPath()\n\tif caller != valopersRealmPath {\n\t\tpanic(\"caller realm must be \" + valopersRealmPath + \", got \" + caller)\n\t}\n}\n\n// NotifyValoperChanged refreshes the cached entry for op. Auth: caller\n// realm must be r/gnops/valopers.\n//\n// READ-ONLY against valopers: by design this function does not call\n// back into valopers (no pull). Valopers pushes the current values in\n// as args. This eliminates the confused-deputy class where v3 → valopers\n// callbacks would make valopers see v3 as PreviousRealm.\nfunc NotifyValoperChanged(cur realm, op address, signingPubKey string, signingAddress address, keepRunning bool) {\n\tassertValopersCaller(0, cur)\n\tvaloperCache.Set(op.String(), cacheEntry{\n\t\tSigningPubKey:  signingPubKey,\n\t\tSigningAddress: signingAddress,\n\t\tKeepRunning:    keepRunning,\n\t})\n}\n\n// RotateValoperSigningKey applies a signing-key rotation to the\n// effective valset and publishes the new full set via sysparams.\n// Auth: caller realm must be r/gnops/valopers.\n//\n// Body is read-modify-write against sysparams.GetValsetEffective so\n// concurrent same-block writers (other rotations or GovDAO executors)\n// accumulate instead of clobbering. Mirrors the executor pattern in\n// validators.gno.\n//\n// Idempotent: if the rotating operator's old signing address is not\n// currently in the effective valset (e.g., they were removed), the\n// rotation is a no-op at the sysparams level — valopers' profile and\n// signingRegistry already record the new key. Either replays cleanly.\n//\n// Emits ValoperRotated event with op + old/new addresses + height.\nfunc RotateValoperSigningKey(cur realm, op address, oldPubKey, newPubKey string) {\n\tassertValopersCaller(0, cur)\n\n\toldAddr, err := chain.PubKeyAddress(oldPubKey)\n\tif err != nil {\n\t\tpanic(\"invalid oldPubKey: \" + err.Error())\n\t}\n\tnewAddr, err := chain.PubKeyAddress(newPubKey)\n\tif err != nil {\n\t\tpanic(\"invalid newPubKey: \" + err.Error())\n\t}\n\n\tbaseline := sysparams.GetValsetEffective()\n\tset := make(map[address]validators.Validator, len(baseline))\n\tfor _, v := range baseline {\n\t\tset[v.Address] = v\n\t}\n\n\t// The rotating operator must currently be in the active set; if\n\t// not (operator removed before rotating), nothing to publish.\n\t// Valopers-side state has already been updated regardless.\n\tprev, ok := set[oldAddr]\n\tif !ok {\n\t\tchain.Emit(\n\t\t\t\"ValoperRotated\",\n\t\t\t\"op\", op.String(),\n\t\t\t\"oldAddr\", oldAddr.String(),\n\t\t\t\"newAddr\", newAddr.String(),\n\t\t\t\"height\", strconv.FormatInt(runtime.ChainHeight(), 10),\n\t\t\t\"applied\", \"false\",\n\t\t)\n\t\treturn\n\t}\n\n\tdelete(set, oldAddr)\n\tset[newAddr] = validators.Validator{\n\t\tAddress:     newAddr,\n\t\tPubKey:      newPubKey,\n\t\tVotingPower: prev.VotingPower,\n\t}\n\n\t// Defense-in-depth: a delete+insert in this branch always leaves\n\t// at least one entry (the freshly inserted newAddr), so an empty\n\t// set is unreachable today. Panic explicitly anyway: a future\n\t// refactor of this body that ends up publishing an empty set\n\t// would otherwise be silently swallowed by the EndBlocker\n\t// (which logs and clears dirty for empty publishes), masking\n\t// the regression.\n\tif len(set) == 0 {\n\t\tpanic(\"rotation would empty the validator set; refused to keep consensus liveness\")\n\t}\n\n\tentries := make([]string, 0, len(set))\n\tfor _, v := range set {\n\t\tentries = append(entries, v.PubKey+\":\"+strconv.FormatUint(v.VotingPower, 10))\n\t}\n\tsort.Strings(entries)\n\tsysparams.SetValsetProposal(cross(cur), entries)\n\n\tchain.Emit(\n\t\t\"ValoperRotated\",\n\t\t\"op\", op.String(),\n\t\t\"oldAddr\", oldAddr.String(),\n\t\t\"newAddr\", newAddr.String(),\n\t\t\"height\", strconv.FormatInt(runtime.ChainHeight(), 10),\n\t\t\"applied\", \"true\",\n\t)\n}\n\n// AssertGenesisValopersConsistent panics if any entry in valset:current\n// (the seeded genesis valset) lacks a corresponding valoperCache profile\n// whose SigningAddress matches.\n//\n// **Genesis-mode only.** The function refuses to run unless\n// runtime.ChainHeight() == 0. This is the documented intended usage\n// (last migration .jsonl tx, before any block has been produced) and\n// also closes a post-genesis MsgCall DoS surface — without the guard,\n// an attacker could pay gas to repeatedly invoke an O(N) iteration\n// over valoperCache + valset:current after the chain is live.\n//\n// gnoland's InitChainer auto-runs this assertion at end of\n// genesis-mode replay when GnoGenesisState.PastChainIDs is non-empty;\n// failure aborts the boot unconditionally. valoper-seed and\n// hand-crafted migration .jsonls do NOT need to emit the call\n// themselves.\n//\n// Crossing function: callable via MsgCall (only at genesis-mode).\n// Doesn't mutate state — pure invariant check. Inverse direction\n// (every valoperCache entry must have a corresponding valset:current\n// entry) is intentionally NOT checked: extra valoper profiles\n// registered without immediate valset inclusion are a normal\n// post-genesis state.\nfunc AssertGenesisValopersConsistent(cur realm) {\n\tif runtime.ChainHeight() != 0 {\n\t\tpanic(\"AssertGenesisValopersConsistent is only callable during genesis-mode replay (ChainHeight()==0)\")\n\t}\n\n\t// Collect the signing addresses present in valoperCache.\n\tseen := map[string]bool{}\n\tvaloperCache.Iterate(\"\", \"\", func(_ string, raw any) bool {\n\t\tentry := raw.(cacheEntry)\n\t\tseen[entry.SigningAddress.String()] = true\n\t\treturn false\n\t})\n\n\t// Every entry in valset:current must appear in seen.\n\tfor _, v := range sysparams.GetValsetEntries() {\n\t\tif !seen[v.Address.String()] {\n\t\t\tpanic(\"genesis-validator \" + v.Address.String() + \" has no corresponding valoper profile (signing address not in valoperCache)\")\n\t\t}\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/validators/v3\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"proposal.gno","body":"package validators\n\nimport (\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"chain\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao\"\n\tsysparams \"gno.land/r/sys/params\"\n)\n\n// ValoperChange is the operator-keyed input shape for the v3 valset\n// proposal builder. Power=0 removes; Power\u003e0 adds (or upserts the\n// power on an op already in the active set — Tendermint's natural\n// ValidatorUpdate semantics).\n//\n// Each operator may appear AT MOST ONCE per proposal; duplicates are\n// rejected at create-time.\ntype ValoperChange struct {\n\tOperatorAddress address\n\tPower           uint64\n}\n\nfunc NewValoperChange(operatorAddress address, power uint64) ValoperChange {\n\treturn ValoperChange{\n\t\tOperatorAddress: operatorAddress,\n\t\tPower:           power,\n\t}\n}\n\nconst errNoValoperChanges = \"no valoper changes proposed\"\n\n// NewValidatorProposalRequest builds a GovDAO proposal that, when\n// executed, applies the deltas to the chain's effective valset and\n// publishes the new full set via SetValsetProposal.\n//\n// NON-CROSSING (no `cur realm`). Direct MsgCall is unsupported;\n// proposers route through r/gnops/valopers/proposal's facade\n// (which IS crossing and accepts user txs).\n//\n// Validation at creation time:\n//   - Each operator may appear AT MOST ONCE in changes; duplicates\n//     panic. Power changes for an op already in the active set use\n//     a single {op, newPower} entry (upsert), not the legacy\n//     remove/re-add pair.\n//   - Every ValoperChange's OperatorAddress must exist in\n//     valoperCache. Unknown operators panic.\n//   - Adds (Power \u003e 0) require KeepRunning=true. An op that has\n//     called UpdateKeepRunning(false) signals opt-out; no proposal\n//     can keep them in the active set, period.\n//\n// Pubkey resolution at execution time: the executor callback\n// re-reads valoperCache for each entry to capture the CURRENT\n// signing pubkey/address — not the creation-time one. Defends\n// against a stale (now-retired) key publication if the operator\n// rotated while the proposal sat in GovDAO. Also re-checks\n// KeepRunning so an operator flipping to KeepRunning=false between\n// propose-create and propose-execute is honored. Removes are\n// unaffected (operator address is the lookup key, not signing\n// address).\n//\n// Emits ValidatorAdded / ValidatorRemoved events per entry on\n// successful execution. (Power-upsert on an existing op also emits\n// ValidatorAdded with the new power.)\nfunc NewValidatorProposalRequest(cur realm, changes []ValoperChange, title, description string) dao.ProposalRequest {\n\tif len(changes) == 0 {\n\t\tpanic(errNoValoperChanges)\n\t}\n\ttitle = strings.TrimSpace(title)\n\tif title == \"\" {\n\t\tpanic(\"proposal title is empty\")\n\t}\n\tif len(changes) \u003e 40 {\n\t\tpanic(\"max number of allowed validators per proposal is 40\")\n\t}\n\n\t// Dedupe: each operator may appear at most once per proposal.\n\t// Power changes are now expressed as a single {op, newPower}\n\t// upsert entry, so the legacy [{op,0},{op,N}] pair is a duplicate\n\t// and rejected.\n\tseen := map[string]bool{}\n\tfor _, c := range changes {\n\t\tkey := c.OperatorAddress.String()\n\t\tif seen[key] {\n\t\t\tpanic(\"duplicate operator in proposal: \" + key)\n\t\t}\n\t\tseen[key] = true\n\t}\n\n\t// Creation-time validation: every operator must exist in cache,\n\t// and adds require KeepRunning=true. KeepRunning=false is a\n\t// binding opt-out; no proposal shape can override it.\n\tfor _, c := range changes {\n\t\trawCache := valoperCache.Get(c.OperatorAddress.String())\n\t\tif rawCache == nil {\n\t\t\tpanic(\"unknown operator: \" + c.OperatorAddress.String())\n\t\t}\n\t\tentry := rawCache.(cacheEntry)\n\t\tif c.Power \u003e 0 \u0026\u0026 !entry.KeepRunning {\n\t\t\tpanic(\"operator \" + c.OperatorAddress.String() + \" has KeepRunning=false; refusing to add (operator must call UpdateKeepRunning(true) first)\")\n\t\t}\n\t}\n\n\t// Render description against creation-time data. Voters see the\n\t// operator addresses being proposed; signing addresses are an\n\t// implementation detail resolved at exec.\n\tvar desc strings.Builder\n\tdesc.WriteString(description)\n\tif len(description) \u003e 0 {\n\t\tdesc.WriteString(\"\\n\\n\")\n\t}\n\tdesc.WriteString(\"## Validator Updates\\n\")\n\tfor _, c := range changes {\n\t\tif c.Power == 0 {\n\t\t\tdesc.WriteString(ufmt.Sprintf(\"- %s: remove\\n\", c.OperatorAddress))\n\t\t} else {\n\t\t\tdesc.WriteString(ufmt.Sprintf(\"- %s: add (power %d)\\n\", c.OperatorAddress, c.Power))\n\t\t}\n\t}\n\n\treturn dao.NewProposalRequest(title, desc.String(), newValoperChangeExecutor(cur, changes))\n}\n\n// newValoperChangeExecutor builds the GovDAO executor that, on\n// approval, applies the captured ValoperChange deltas. Resolves\n// operator → signing addr/pubkey via valoperCache at execution time\n// for adds (so a mid-flight rotation doesn't publish a stale key).\n// Removes resolve the operator's CURRENT signing address (also via\n// cache) — operator-keyed removes are immune to rotation churn.\n//\n// Power\u003e0 is an upsert against the effective valset map (keyed on\n// signing address): if the op is already present under that signing\n// address, the entry's voting power is overwritten. Tendermint\n// natively handles ValidatorUpdates as upserts, so a single-entry\n// power change is the canonical form.\nfunc newValoperChangeExecutor(cur realm, changes []ValoperChange) dao.Executor {\n\tcallback := func(cur realm) error {\n\t\tbaseline := sysparams.GetValsetEffective()\n\t\tset := make(map[address]validators.Validator, len(baseline))\n\t\tfor _, v := range baseline {\n\t\t\tset[v.Address] = v\n\t\t}\n\n\t\tfor _, c := range changes {\n\t\t\trawCache := valoperCache.Get(c.OperatorAddress.String())\n\t\t\tif rawCache == nil {\n\t\t\t\tpanic(\"operator vanished from valoperCache between propose and execute: \" + c.OperatorAddress.String())\n\t\t\t}\n\t\t\tentry := rawCache.(cacheEntry)\n\n\t\t\tif c.Power == 0 {\n\t\t\t\tif _, ok := set[entry.SigningAddress]; !ok {\n\t\t\t\t\tpanic(\"validator does not exist: \" + entry.SigningAddress.String())\n\t\t\t\t}\n\t\t\t\tdelete(set, entry.SigningAddress)\n\t\t\t\tchain.Emit(\n\t\t\t\t\t\"ValidatorRemoved\",\n\t\t\t\t\t\"op\", c.OperatorAddress.String(),\n\t\t\t\t\t\"signingAddr\", entry.SigningAddress.String(),\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Race-safety: operator may have flipped KeepRunning=false\n\t\t\t// between propose-create and propose-execute. Re-check.\n\t\t\t// The opt-out is binding regardless of proposal shape.\n\t\t\tif !entry.KeepRunning {\n\t\t\t\tpanic(\"operator \" + c.OperatorAddress.String() + \" has KeepRunning=false at execution; refusing to add\")\n\t\t\t}\n\n\t\t\t// Upsert at the current signing address. If the entry was\n\t\t\t// already present (single-entry power change on an active\n\t\t\t// validator), this overwrites the prior power.\n\t\t\tset[entry.SigningAddress] = validators.Validator{\n\t\t\t\tAddress:     entry.SigningAddress,\n\t\t\t\tPubKey:      entry.SigningPubKey,\n\t\t\t\tVotingPower: c.Power,\n\t\t\t}\n\t\t\tchain.Emit(\n\t\t\t\t\"ValidatorAdded\",\n\t\t\t\t\"op\", c.OperatorAddress.String(),\n\t\t\t\t\"signingAddr\", entry.SigningAddress.String(),\n\t\t\t\t\"power\", strconv.FormatUint(c.Power, 10),\n\t\t\t)\n\t\t}\n\n\t\t// Liveness floor: refuse to publish an empty set.\n\t\tif len(set) == 0 {\n\t\t\tpanic(\"valset proposal would empty the validator set; refused to keep consensus liveness\")\n\t\t}\n\n\t\tentries := make([]string, 0, len(set))\n\t\tfor _, v := range set {\n\t\t\tentries = append(entries, v.PubKey+\":\"+strconv.FormatUint(v.VotingPower, 10))\n\t\t}\n\t\tsort.Strings(entries)\n\t\tsysparams.SetValsetProposal(cross(cur), entries)\n\t\treturn nil\n\t}\n\n\treturn dao.NewSimpleExecutor(0, cur, callback, \"\")\n}\n"},{"name":"validators.gno","body":"// Package validators implements on-chain validator set management\n// through Proof of Authority. The realm exposes a public proposal\n// constructor for GovDAO; on approval, the proposal callback applies\n// the captured deltas to the chain's effective valset and publishes\n// the new full set via gno.land/r/sys/params. The chain's EndBlocker\n// reads the result on the next block and propagates to consensus.\n//\n// No in-realm validator state. All reads go through\n// sysparams.GetValsetEffective (proposed-if-dirty, else current).\npackage validators\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\n\tsysparams \"gno.land/r/sys/params\"\n)\n\n// Operator-keyed proposal builder lives in proposal.gno\n// (NewValidatorProposalRequest + newValoperChangeExecutor). The legacy\n// signing-keyed NewProposalRequest was removed: every valid\n// signing-keyed input is also a valid operator-keyed input under\n// always-on valoper enforcement.\n\n// IsValidator returns true if addr is part of the effective validator\n// set (proposed if a v3 proposal is awaiting EndBlocker, else\n// current).\nfunc IsValidator(addr address) bool {\n\tfor _, v := range sysparams.GetValsetEffective() {\n\t\tif v.Address == addr {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetValidator returns the validator with the given address from the\n// effective set; panics if absent.\nfunc GetValidator(addr address) validators.Validator {\n\tfor _, v := range sysparams.GetValsetEffective() {\n\t\tif v.Address == addr {\n\t\t\treturn v\n\t\t}\n\t}\n\tpanic(\"validator not found\")\n}\n\n// GetValidators returns the effective validator set.\nfunc GetValidators() []validators.Validator {\n\treturn sysparams.GetValsetEffective()\n}\n\n// Render displays the effective validator set.\nfunc Render(string) string {\n\tvar sb strings.Builder\n\th := runtime.ChainHeight()\n\tset := sysparams.GetValsetEffective()\n\tsb.WriteString(ufmt.Sprintf(\"## Valset at height %d\\n\\n\", h))\n\tif len(set) == 0 {\n\t\tsb.WriteString(\"Valset is empty.\\n\")\n\t\treturn sb.String()\n\t}\n\tfor i, v := range set {\n\t\tsb.WriteString(ufmt.Sprintf(\"- #%d: %s (%d)\\n\", i, v.Address.String(), v.VotingPower))\n\t}\n\treturn sb.String()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"valopers","path":"gno.land/r/gnops/valopers","files":[{"name":"admin.gno","body":"package valopers\n\nimport (\n\t\"gno.land/p/moul/authz\"\n)\n\nvar auth *authz.Authorizer\n\nfunc Auth() *authz.Authorizer {\n\treturn auth\n}\n\nfunc updateInstructions(_ int, rlm realm, newInstructions string) {\n\terr := auth.DoByCurrent(0, rlm, \"update-instructions\", func() error {\n\t\tinstructions = newInstructions\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc NewInstructionsProposalCallback(newInstructions string) func(realm) error {\n\tcb := func(cur realm) error {\n\t\tupdateInstructions(0, cur, newInstructions)\n\t\treturn nil\n\t}\n\n\treturn cb\n}\n\n// The min-fee callback was removed: the fee now lives in sysparams\n// under node:valoper:register_fee, and\n// proposal.ProposeNewMinFeeProposalRequest delegates to\n// sys/params.NewSysParamUint64PropRequest. Removing avoids the\n// forward-compat hazard of a no-op shim with no caller-auth gating.\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnops/valopers\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"init.gno","body":"package valopers\n\nimport (\n\t\"gno.land/p/moul/authz\"\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nfunc init() {\n\tvalopers = avl.NewTree()\n\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gnops/valopers\",\n\t\t\tfunc(_ string, action authz.PrivilegedAction) error {\n\t\t\t\treturn action()\n\t\t\t},\n\t\t),\n\t)\n\n\tinstructions = `\n# Welcome to the **Valopers** realm\n\n## 📌 Purpose of this Contract\n\nThe **Valopers** contract is designed to maintain a registry of **validator profiles**. This registry provides essential information to **GovDAO members**, enabling them to make informed decisions when voting on the inclusion of new validators into the **valset**.\n\nBy registering your validator profile, you contribute to a transparent and well-informed governance process within **gno.land**.\n\n---\n\n## 📝 How to Register Your Validator Node\n\nTo add your validator node to the registry, use the [**Register**](` + txlink.Call(\"Register\") + `) function with the following parameters:\n\n- **Moniker** (Validator Name)\n  - Must be **human-readable**\n  - **Max length**: **32 characters**\n  - **Allowed characters**: Letters, numbers, spaces, hyphens (**-**), and underscores (**_**)\n  - **No special characters** at the beginning or end\n\n- **Description** (Introduction \u0026 Validator Details)\n  - **Max length**: **2048 characters**\n  - Must include answers to the questions listed below\n\n- **Server Type** (Infrastructure Type)\n  - Must be one of the following values:\n    - **cloud**: For validators running on cloud infrastructure (AWS, GCP, Azure, etc.)\n    - **on-prem**: For validators running on on-premises infrastructure\n    - **data-center**: For validators running in dedicated data centers\n\n- **Operator Address**\n  - The ` + \"`g1...`\" + ` address of your operator account (from your ` + \"`gnokey`\" + ` keyring)\n  - **Must be controlled by the signer** of this transaction — the realm rejects the call if the signer doesn't control that address\n\n- **Validator Consensus Public Key**\n  - Your validator node's consensus public key, in the ` + \"`gpub1...`\" + ` format\n  - Retrieve it by running: ` + \"`gnoland secrets get validator_key`\" + `\n\n### ✍️ Required Information for the Description\n\nPlease provide detailed answers to the following questions to ensure transparency and improve your chances of being accepted:\n\n1. The name of your validator\n2. Networks you are currently validating and your total AuM (assets under management)\n3. Links to your **digital presence** (website, social media, etc.). Please include your Discord handle to be added to our main comms channel, the gno.land valoper Discord channel.\n4. Contact details\n5. Why are you interested in validating on **gno.land**?\n6. What contributions have you made or are willing to make to **gno.land**?\n\n---\n\n## 🔄 Updating Your Validator Information\n\nAfter registration, you can update your validator details using the **update functions** provided by the contract.\n\n---\n\n## 📢 Submitting a Proposal to Join the Validator Set\n\nOnce you're satisfied with your **valoper** profile, you need to notify GovDAO; only a GovDAO member can submit a proposal to add you to the validator set.\n\nIf you are a GovDAO member, you can nominate yourself by executing the following function: [**r/gnops/valopers/proposal.ProposeNewValidator**](` + txlink.Realm(\"gno.land/r/gnops/valopers/proposal\").Call(\"ProposeNewValidator\") + `)\n\nThis will initiate a governance process where **GovDAO** members will vote on your proposal.\n\n---\n\n🚀 **Register now and become a part of gno.land’s validator ecosystem!**\n\nRead more: [How to become a validator](https://github.com/gnolang/gno/tree/master/gno.land/cmd/gnoland#become-a-validator)\n\nDisclaimer: Please note, registering your validator profile and/or validating on testnets does not guarantee a validator slot on the gno.land beta mainnet. However, active participation and contributions to testnets will help establish credibility and may improve your chances for future validator acceptance. The initial validator amount and valset will ultimately be selected through GovDAO governance proposals and acceptance.\n\n---\n\n`\n}\n"},{"name":"valopers.gno","body":"// Package valopers is designed around the permissionless lifecycle of valoper profiles.\npackage valopers\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"crypto/bech32\"\n\t\"errors\"\n\t\"regexp\"\n\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/combinederr/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ownable/v0/exts/authorizable\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\tsysparams \"gno.land/r/sys/params\"\n\tvalidators \"gno.land/r/sys/validators/v3\"\n)\n\nconst (\n\tMonikerMaxLength     = 32\n\tDescriptionMaxLength = 2048\n\n\t// Valid server types\n\tServerTypeCloud      = \"cloud\"\n\tServerTypeOnPrem     = \"on-prem\"\n\tServerTypeDataCenter = \"data-center\"\n)\n\nvar (\n\tErrValoperExists        = errors.New(\"valoper already exists\")\n\tErrValoperMissing       = errors.New(\"valoper does not exist\")\n\tErrInvalidAddress       = errors.New(\"invalid address\")\n\tErrInvalidMoniker       = errors.New(\"moniker is not valid\")\n\tErrInvalidDescription   = errors.New(\"description is not valid\")\n\tErrInvalidServerType    = errors.New(\"server type is not valid\")\n\tErrOperatorSquatGuard   = errors.New(\"post-genesis: caller must equal operator address\")\n\tErrSigningKeyTaken      = errors.New(\"signing address already in registry (active or retired)\")\n\tErrFrontrunValidator    = errors.New(\"post-genesis: signing address is already an active validator\")\n\tErrRotationThrottled    = errors.New(\"rotation throttled: try again later\")\n\tErrRegistryEntryMissing = errors.New(\"signing address has no active registry entry (corrupted state)\")\n\tErrDisallowedPubKeyType = errors.New(\"consensus pubkey type is not allowed for validators\")\n)\n\nvar (\n\tvalopers     *avl.Tree // operator-address -\u003e Valoper\n\tinstructions string    // markdown instructions for valoper's registration\n\n\t// signingRegistry maps SigningAddress.String() -\u003e regEntry.\n\t// Permanently retains retired entries to prevent key reuse and\n\t// to support future slashing-attribution by signing address.\n\tsigningRegistry = bptree.NewBPTree32()\n\n\tmonikerMaxLengthMiddle = ufmt.Sprintf(\"%d\", MonikerMaxLength-2)\n\tvalidateMonikerRe      = regexp.MustCompile(`^[a-zA-Z0-9][\\w -]{0,` + monikerMaxLengthMiddle + `}[a-zA-Z0-9]$`) // 32 characters, including spaces, hyphens or underscores in the middle\n)\n\n// regEntry tracks signing-address -\u003e operator with retirement metadata.\n// retiredAtHeight == 0 means the entry is currently active for the operator.\ntype regEntry struct {\n\tOperatorAddress    address\n\tRegisteredAtHeight int64\n\tRetiredAtHeight    int64\n}\n\n// Valoper represents a validator operator profile.\ntype Valoper struct {\n\tMoniker     string // A human-readable name\n\tDescription string // A description and details about the valoper\n\tServerType  string // The type of server (cloud/on-prem/data-center)\n\n\tOperatorAddress address // operator identity, profile key, stable across rotations\n\tSigningPubKey   string  // current consensus signing pubkey (bech32 gpub1...)\n\tSigningAddress  address // = chain.PubKeyAddress(SigningPubKey)\n\n\tLastRotationHeight int64 // throttle anchor for UpdateSigningKey\n\n\tKeepRunning bool // operator wants this validator running in the active set\n\n\tauth *authorizable.Authorizable\n}\n\nfunc (v Valoper) Auth() *authorizable.Authorizable {\n\treturn v.auth\n}\n\nfunc AddToAuthList(cur realm, addr address, member address) {\n\tv := GetByAddr(addr)\n\tif err := v.Auth().AddToAuthList(0, cur, member); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc DeleteFromAuthList(cur realm, addr address, member address) {\n\tv := GetByAddr(addr)\n\tif err := v.Auth().DeleteFromAuthList(0, cur, member); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Register registers a new valoper. The `addr` parameter is the\n// operator address (stable identity, profile key); `pubKey` is the\n// consensus signing pubkey, from which the signing address is derived.\n//\n// Auth shape:\n//   - Post-genesis: OriginCaller must equal addr (operator-slot squat\n//     guard). Genesis-mode replay (ChainHeight()==0) bypasses, so\n//     migration .jsonl txs and historical Register replays succeed.\n//   - Signing-address uniqueness: derived(pubKey) must not already be\n//     in signingRegistry, active or retired.\n//   - Front-running guard: post-genesis, derived(pubKey) must not\n//     already be an active validator (a fresh registration cannot\n//     squat on the consensus address of an existing validator).\n//\n// Why OriginCaller==addr is sufficient (no IsUserCall): squatting\n// requires the attacker to be able to satisfy OriginCaller==victim,\n// which requires the victim's signing key. r/sys/namereg/v1.Register\n// also gates on IsUserCall, but that's because IT reads\n// unsafe.OriginSend() for the anti-squatting payment and IsUserCall\n// is needed to ensure the OriginSend envelope reflects what landed at\n// this realm rather than a phantom payment from a previous frame.\n// valopers.Register has no per-call payment-receipt check (fees are\n// validated against banker.OriginSend in a way that's symmetric to\n// IsUserCall via direct comparison), so the IsUserCall tightening\n// would only block legitimate `maketx run` flows (operator-authored\n// scripts that legitimately set OriginCaller==operator) without\n// adding identity-squat protection.\n//\n// Auth-list seeding: the profile's Authorizable owner is set to addr\n// (NOT OriginCaller). At H\u003e0 the squat guard makes them equal anyway;\n// at H==0 the deployer pattern (one signer registers many operators)\n// requires owner == addr so each operator can manage their own profile\n// post-genesis without needing the deployer's auth.\nfunc Register(cur realm, moniker string, description string, serverType string, addr address, pubKey string) {\n\t// Operator-slot squat guard.\n\tif runtime.ChainHeight() \u003e 0 \u0026\u0026 unsafe.OriginCaller() != addr {\n\t\tpanic(ErrOperatorSquatGuard)\n\t}\n\n\t// Fee enforcement (read from sysparams; defaults to 0 until\n\t// governance raises it post-transfer-enablement).\n\tif fee := sysparams.GetValoperRegisterFee(); fee \u003e 0 {\n\t\tminFee := chain.NewCoin(\"ugnot\", int64(fee))\n\t\tsentCoins := unsafe.OriginSend()\n\t\tif len(sentCoins) != 1 || sentCoins[0].IsLT(minFee) {\n\t\t\tpanic(ufmt.Sprintf(\"payment must not be less than %d%s\", minFee.Amount, minFee.Denom))\n\t\t}\n\t}\n\n\t// Check if the valoper is already registered.\n\tif isValoper(addr) {\n\t\tpanic(ErrValoperExists)\n\t}\n\n\t// Reject disallowed key types early (else the EndBlocker drops it silently).\n\tassertPubKeyTypeAllowed(pubKey)\n\n\t// Derive the consensus signing address from the pubkey.\n\tsigningAddr, err := chain.PubKeyAddress(pubKey)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Signing-address uniqueness across all profiles, ever.\n\tif signingRegistry.Has(signingAddr.String()) {\n\t\tpanic(ErrSigningKeyTaken)\n\t}\n\n\t// Front-running guard: post-genesis, the signing address must\n\t// not already be an active validator.\n\tif runtime.ChainHeight() \u003e 0 \u0026\u0026 validators.IsValidator(signingAddr) {\n\t\tpanic(ErrFrontrunValidator)\n\t}\n\n\tv := Valoper{\n\t\tMoniker:            moniker,\n\t\tDescription:        description,\n\t\tServerType:         serverType,\n\t\tOperatorAddress:    addr,\n\t\tSigningPubKey:      pubKey,\n\t\tSigningAddress:     signingAddr,\n\t\tLastRotationHeight: runtime.ChainHeight(),\n\t\tKeepRunning:        true,\n\t\tauth:               authorizable.New(ownable.NewWithAddress(addr)),\n\t}\n\n\tif err := v.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Save the valoper to the set.\n\tvalopers.Set(v.OperatorAddress.String(), v)\n\n\t// Insert into the signing-address registry.\n\tsigningRegistry.Set(signingAddr.String(), regEntry{\n\t\tOperatorAddress:    addr,\n\t\tRegisteredAtHeight: runtime.ChainHeight(),\n\t\tRetiredAtHeight:    0,\n\t})\n\n\t// Refresh v3's cache for this operator.\n\tvalidators.NotifyValoperChanged(cross(cur), addr, v.SigningPubKey, v.SigningAddress, v.KeepRunning)\n}\n\n// UpdateMoniker updates an existing valoper's moniker.\nfunc UpdateMoniker(cur realm, addr address, moniker string) {\n\t// Check that the moniker is not empty.\n\tif err := validateMoniker(moniker); err != nil {\n\t\tpanic(err)\n\t}\n\n\tv := GetByAddr(addr)\n\n\t// Check that the caller has permissions.\n\tv.Auth().AssertPreviousOnAuthList(0, cur)\n\n\t// Update the moniker.\n\tv.Moniker = moniker\n\n\t// Save the valoper info.\n\tvalopers.Set(addr.String(), v)\n}\n\n// UpdateDescription updates an existing valoper's description.\nfunc UpdateDescription(cur realm, addr address, description string) {\n\t// Check that the description is not empty.\n\tif err := validateDescription(description); err != nil {\n\t\tpanic(err)\n\t}\n\n\tv := GetByAddr(addr)\n\n\t// Check that the caller has permissions.\n\tv.Auth().AssertPreviousOnAuthList(0, cur)\n\n\t// Update the description.\n\tv.Description = description\n\n\t// Save the valoper info.\n\tvalopers.Set(addr.String(), v)\n}\n\n// UpdateKeepRunning updates an existing valoper's active status.\n// Calls v3.NotifyValoperChanged because the cache stores KeepRunning.\nfunc UpdateKeepRunning(cur realm, addr address, keepRunning bool) {\n\tv := GetByAddr(addr)\n\n\t// Check that the caller has permissions.\n\tv.Auth().AssertPreviousOnAuthList(0, cur)\n\n\t// Update status.\n\tv.KeepRunning = keepRunning\n\n\t// Save the valoper info.\n\tvalopers.Set(addr.String(), v)\n\n\t// Refresh v3's cache (KeepRunning is one of the cached fields).\n\tvalidators.NotifyValoperChanged(cross(cur), addr, v.SigningPubKey, v.SigningAddress, v.KeepRunning)\n}\n\n// UpdateServerType updates an existing valoper's server type.\nfunc UpdateServerType(cur realm, addr address, serverType string) {\n\t// Check that the server type is valid.\n\tif err := validateServerType(serverType); err != nil {\n\t\tpanic(err)\n\t}\n\n\tv := GetByAddr(addr)\n\n\t// Check that the caller has permissions.\n\tv.Auth().AssertPreviousOnAuthList(0, cur)\n\n\t// Update server type.\n\tv.ServerType = serverType\n\n\t// Save the valoper info.\n\tvalopers.Set(addr.String(), v)\n}\n\n// UpdateSigningKey rotates an operator's consensus signing key.\n//\n// Auth: caller must be on the operator's auth list (defaults to\n// operator at Register time; extendable via AddToAuthList).\n//\n// Invariants checked at entry:\n//   - throttle: ChainHeight() - v.LastRotationHeight \u003e=\n//     rotationPeriodBlocks\n//   - signingRegistry uniqueness: derived(newPubKey) not in registry\n//     (active OR retired); permanently blocks key reuse\n//   - fee: unsafe.OriginSend() \u003e= rotationFee (mirrors Register's\n//     fee-check pattern)\n//\n// Effect: profile's SigningPubKey/SigningAddress/LastRotationHeight\n// updated; old registry entry marked retired (retiredAtHeight =\n// ChainHeight()); new entry inserted into signingRegistry; v3 emits\n// remove+add to sysparams via RotateValoperSigningKey; v3 cache\n// refreshed via NotifyValoperChanged. Rotation lands in consensus\n// at H+2.\n//\n// Atomicity: Gno tx atomicity rolls back all state if any step\n// panics. If v3.RotateValoperSigningKey panics, the registry insert\n// and profile mutation revert with it.\nfunc UpdateSigningKey(cur realm, addr address, newPubKey string) {\n\tv := GetByAddr(addr)\n\n\t// Auth: caller must be on operator's auth list.\n\tv.Auth().AssertPreviousOnAuthList(0, cur)\n\n\t// Throttle: limit one rotation per rotation_period_blocks per\n\t// operator (per profile, not per caller — multi-member auth lists\n\t// can't multiplicative-rotate).\n\theight := runtime.ChainHeight()\n\tif height-v.LastRotationHeight \u003c sysparams.GetValoperRotationPeriodBlocks() {\n\t\tpanic(ErrRotationThrottled)\n\t}\n\n\t// Fee: enforce only if non-zero (matches Register's pattern;\n\t// rotation_fee defaults to zero pre-transfer-enablement).\n\tif fee := sysparams.GetValoperRotationFee(); fee \u003e 0 {\n\t\tminFee := chain.NewCoin(\"ugnot\", int64(fee))\n\t\tsentCoins := unsafe.OriginSend()\n\t\tif len(sentCoins) != 1 || sentCoins[0].IsLT(minFee) {\n\t\t\tpanic(ufmt.Sprintf(\"payment must not be less than %d%s\", minFee.Amount, minFee.Denom))\n\t\t}\n\t}\n\n\t// Reject disallowed key types early (else the EndBlocker drops it silently).\n\tassertPubKeyTypeAllowed(newPubKey)\n\n\t// Derive the new signing address from the new pubkey.\n\tnewSigningAddr, err := chain.PubKeyAddress(newPubKey)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// signingRegistry uniqueness: new key must not have ever been\n\t// registered (active or retired).\n\tif signingRegistry.Has(newSigningAddr.String()) {\n\t\tpanic(ErrSigningKeyTaken)\n\t}\n\n\t// Front-running guard: the derived signing address must not already\n\t// be an active validator. Mirrors the same guard in Register\n\t// (ErrFrontrunValidator). signingRegistry uniqueness above only\n\t// blocks signing addresses that previously went through Register or\n\t// UpdateSigningKey — genesis-seeded validators bypassed both, so\n\t// their signing addresses are absent from signingRegistry. Without\n\t// this check, a valoper could rotate onto such a slot and hijack\n\t// it: v3.RotateValoperSigningKey would overwrite the active entry\n\t// with this operator's claim, and a subsequent govDAO remove-op\n\t// proposal would then delete it.\n\tif validators.IsValidator(newSigningAddr) {\n\t\tpanic(ErrFrontrunValidator)\n\t}\n\n\t// Remember the previous signing key for the v3 cross-call.\n\toldPubKey := v.SigningPubKey\n\toldSigningAddr := v.SigningAddress\n\n\t// Mark the old registry entry retired. The entry must exist —\n\t// it was inserted at Register time.\n\trawOld := signingRegistry.Get(oldSigningAddr.String())\n\tif rawOld == nil {\n\t\tpanic(ErrRegistryEntryMissing)\n\t}\n\toldEntry := rawOld.(regEntry)\n\toldEntry.RetiredAtHeight = height\n\tsigningRegistry.Set(oldSigningAddr.String(), oldEntry)\n\n\t// Insert the new entry as active.\n\tsigningRegistry.Set(newSigningAddr.String(), regEntry{\n\t\tOperatorAddress:    addr,\n\t\tRegisteredAtHeight: height,\n\t\tRetiredAtHeight:    0,\n\t})\n\n\t// Update the profile.\n\tv.SigningPubKey = newPubKey\n\tv.SigningAddress = newSigningAddr\n\tv.LastRotationHeight = height\n\tvalopers.Set(addr.String(), v)\n\n\t// Apply to consensus via v3, then refresh v3's cache view of the\n\t// profile. Order matters only in that both must complete; tx\n\t// atomicity rolls back together on any panic.\n\tvalidators.RotateValoperSigningKey(cross(cur), addr, oldPubKey, newPubKey)\n\tvalidators.NotifyValoperChanged(cross(cur), addr, v.SigningPubKey, v.SigningAddress, v.KeepRunning)\n}\n\n// GetByAddr fetches the valoper using the operator address, if present.\nfunc GetByAddr(addr address) Valoper {\n\tvaloperRaw := valopers.Get(addr.String())\n\tif valoperRaw == nil {\n\t\tpanic(ErrValoperMissing)\n\t}\n\n\treturn valoperRaw.(Valoper)\n}\n\n// Render renders the current valoper set.\n// \"/r/gnops/valopers\" lists all valopers, paginated.\n// \"/r/gnops/valopers:addr\" shows the detail for the valoper with the addr.\nfunc Render(fullPath string) string {\n\treq := realmpath.Parse(fullPath)\n\tif req.Path == \"\" {\n\t\treturn renderHome(fullPath)\n\t} else {\n\t\taddr := req.Path\n\t\tif len(addr) \u003c 2 || addr[:2] != \"g1\" {\n\t\t\treturn \"invalid address \" + addr\n\t\t}\n\t\tvaloperRaw := valopers.Get(addr)\n\t\tif valoperRaw == nil {\n\t\t\treturn \"unknown address \" + addr\n\t\t}\n\t\tv := valoperRaw.(Valoper)\n\t\treturn \"Valoper's details:\\n\" + v.Render()\n\t}\n}\n\nfunc renderHome(path string) string {\n\t// if there are no valopers, display instructions\n\tif valopers.Size() == 0 {\n\t\treturn ufmt.Sprintf(\"%s\\n\\nNo valopers to display.\", instructions)\n\t}\n\n\tpage := pager.NewPager(valopers, 50, false).MustGetPageByPath(path)\n\n\toutput := \"\"\n\n\t// if we are on the first page, display instructions\n\tif page.PageNumber == 1 {\n\t\toutput += ufmt.Sprintf(\"%s\\n\\n\", instructions)\n\t}\n\n\tfor _, item := range page.Items {\n\t\tv := item.Value.(Valoper)\n\t\toutput += ufmt.Sprintf(\" * [%s](/r/gnops/valopers:%s) - [profile](/r/demo/profile:u/%s)\\n\",\n\t\t\tv.Moniker, v.OperatorAddress, v.OperatorAddress)\n\t}\n\n\toutput += \"\\n\"\n\toutput += page.Picker(path)\n\treturn output\n}\n\n// Validate checks if the fields of the Valoper are valid.\nfunc (v *Valoper) Validate() error {\n\terrs := \u0026combinederr.CombinedError{}\n\n\terrs.Add(validateMoniker(v.Moniker))\n\terrs.Add(validateDescription(v.Description))\n\terrs.Add(validateServerType(v.ServerType))\n\terrs.Add(validateBech32(v.OperatorAddress))\n\terrs.Add(validatePubKey(v.SigningPubKey))\n\n\tif errs.Size() == 0 {\n\t\treturn nil\n\t}\n\n\treturn errs\n}\n\n// Render renders a single valoper with their information.\nfunc (v Valoper) Render() string {\n\toutput := ufmt.Sprintf(\"## %s\\n\", v.Moniker)\n\n\tif v.Description != \"\" {\n\t\toutput += ufmt.Sprintf(\"%s\\n\\n\", v.Description)\n\t}\n\n\toutput += ufmt.Sprintf(\"- Operator Address: %s\\n\", v.OperatorAddress.String())\n\toutput += ufmt.Sprintf(\"- Signing Address: %s\\n\", v.SigningAddress.String())\n\toutput += ufmt.Sprintf(\"- Signing PubKey: %s\\n\", v.SigningPubKey)\n\toutput += ufmt.Sprintf(\"- Server Type: %s\\n\\n\", v.ServerType)\n\toutput += ufmt.Sprintf(\"[Profile link](/r/demo/profile:u/%s)\\n\", v.OperatorAddress)\n\n\treturn output\n}\n\n// isValoper checks if the valoper exists.\nfunc isValoper(addr address) bool {\n\treturn valopers.Has(addr.String())\n}\n\n// validateMoniker checks if the moniker is valid.\nfunc validateMoniker(moniker string) error {\n\tif moniker == \"\" {\n\t\treturn ErrInvalidMoniker\n\t}\n\n\tif len(moniker) \u003e MonikerMaxLength {\n\t\treturn ErrInvalidMoniker\n\t}\n\n\tif !validateMonikerRe.MatchString(moniker) {\n\t\treturn ErrInvalidMoniker\n\t}\n\n\treturn nil\n}\n\n// validateDescription checks if the description is valid.\nfunc validateDescription(description string) error {\n\tif description == \"\" {\n\t\treturn ErrInvalidDescription\n\t}\n\n\tif len(description) \u003e DescriptionMaxLength {\n\t\treturn ErrInvalidDescription\n\t}\n\n\treturn nil\n}\n\n// validateBech32 checks if the value is a valid bech32 address.\nfunc validateBech32(addr address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\treturn nil\n}\n\n// validatePubKey checks if the public key is valid.\nfunc validatePubKey(pubKey string) error {\n\tif _, _, err := bech32.DecodeNoLimit(pubKey); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// assertPubKeyTypeAllowed panics if pubKey's type is not in the chain's validator allow-list (empty list accepts any).\nfunc assertPubKeyTypeAllowed(pubKey string) {\n\tallowed := sysparams.GetValsetPubKeyTypes()\n\tif len(allowed) == 0 {\n\t\treturn\n\t}\n\ttypeURL, err := pubKeyTypeURL(pubKey)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfor _, a := range allowed {\n\t\tif a == typeURL {\n\t\t\treturn\n\t\t}\n\t}\n\tpanic(ErrDisallowedPubKeyType)\n}\n\n// pubKeyTypeURL returns the amino type URL (e.g. \"/tm.PubKeyEd25519\") of a bech32 consensus pubkey.\nfunc pubKeyTypeURL(pubKey string) (string, error) {\n\t// gpub exceeds bech32's 90-char cap, so decode without the limit.\n\t_, data5, err := bech32.DecodeNoLimit(pubKey)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdata, err := bech32.ConvertBits(data5, 5, 8, false)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// Type URL is the first amino field: 0x0A \u003clen\u003e \u003ctypeURL\u003e.\n\tif len(data) \u003c 2 || data[0] != 0x0A {\n\t\treturn \"\", errors.New(\"malformed consensus pubkey: \" + pubKey)\n\t}\n\tn := int(data[1])\n\tif n == 0 || len(data) \u003c 2+n {\n\t\treturn \"\", errors.New(\"malformed consensus pubkey: \" + pubKey)\n\t}\n\treturn string(data[2 : 2+n]), nil\n}\n\n// validateServerType checks if the server type is valid.\nfunc validateServerType(serverType string) error {\n\tif serverType != ServerTypeCloud \u0026\u0026\n\t\tserverType != ServerTypeOnPrem \u0026\u0026\n\t\tserverType != ServerTypeDataCenter {\n\t\treturn ErrInvalidServerType\n\t}\n\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"addrset","path":"gno.land/p/nt/addrset/v0","files":[{"name":"addrset.gno","body":"// Package addrset provides a set of blockchain addresses backed by a\n// B+ tree, with a read-only view type for safe cross-realm exposure.\n//\n// It mirrors the gno.land/p/moul/addrset API on a\n// gno.land/p/nt/bptree/v0 backing: a B+ tree packs many entries per\n// persisted node, so a stored address costs ~0.9 KB vs the\n// one-node-per-entry AVL backing's ~2.0 KB (2.2x asymptotically, 1.6x\n// at 10 entries; insert gas ~2.1x less). Prefer this package when sets\n// are part of persisted realm state; the omitted Tree() escape hatch is\n// deliberate, so the backing store never leaks.\n//\n// Two behavioral differences from the AVL-backed moul package, both\n// consequences of the in-place-mutating backing:\n//\n//   - the set must NOT be mutated (Add/Remove) from inside an iteration\n//     callback — the AVL backing's copy-on-write tolerated it, this one\n//     does not;\n//   - do not copy a non-zero Set by value — the copies share live tree\n//     nodes while their roots and sizes diverge (the AVL backing's\n//     copies were independent snapshots).\n//\n// Example:\n//\n//\tvar set addrset.Set // the zero value is an empty, usable set\n//\n//\tset.Add(addr)   // true (newly added)\n//\tset.Has(addr)   // true\n//\tset.Remove(addr) // true (was present)\npackage addrset\n\nimport \"gno.land/p/nt/bptree/v0\"\n\n// Set stores a set of addresses in sorted order. The zero value is an\n// empty, usable set.\ntype Set struct {\n\ttree bptree.BPTree\n}\n\n// Add inserts an address into the set.\n// Returns true if the address was newly added, false if it already existed.\nfunc (s *Set) Add(addr address) bool {\n\treturn !s.tree.Set(string(addr), nil)\n}\n\n// Remove deletes an address from the set.\n// Returns true if the address was found and removed, false if it didn't exist.\nfunc (s *Set) Remove(addr address) bool {\n\t_, removed := s.tree.Remove(string(addr))\n\treturn removed\n}\n\n// Has checks if an address exists in the set.\nfunc (s *Set) Has(addr address) bool {\n\treturn s.tree.Has(string(addr))\n}\n\n// Size returns the number of addresses in the set.\nfunc (s *Set) Size() int {\n\treturn s.tree.Size()\n}\n\n// IterateByOffset walks through addresses in sorted order, starting at\n// the given offset and visiting up to count addresses. The callback\n// returns true to stop iteration. The set must not be modified during\n// iteration (no Add or Remove from the callback).\nfunc (s *Set) IterateByOffset(offset int, count int, cb func(addr address) bool) {\n\ts.tree.IterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(address(key))\n\t})\n}\n\n// ReverseIterateByOffset walks through addresses in reverse (descending)\n// order, starting at the given offset (counted from the end) and\n// visiting up to count addresses. The callback returns true to stop\n// iteration. The set must not be modified during iteration (no Add or\n// Remove from the callback).\nfunc (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr address) bool) {\n\ts.tree.ReverseIterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(address(key))\n\t})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/addrset/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"},{"name":"readonly.gno","body":"package addrset\n\n// ReadonlySet is a read-only view of a *Set. Cross-package callers cannot\n// mutate the underlying set through this type: it exposes no mutator\n// methods and holds the *Set in an unexported field, so a foreign realm\n// can neither reach the set nor invoke Add/Remove on it.\n//\n// A ReadonlySet is a thin handle over the live Set (it does not copy or\n// snapshot), so reads through it always reflect the Set's current contents.\ntype ReadonlySet struct {\n\tset *Set\n}\n\n// NewReadonlySet returns a read-only view of s.\nfunc NewReadonlySet(s *Set) *ReadonlySet {\n\treturn \u0026ReadonlySet{set: s}\n}\n\n// Readonly returns a read-only view of the set.\nfunc (s *Set) Readonly() *ReadonlySet {\n\treturn NewReadonlySet(s)\n}\n\n// Has reports whether addr is in the underlying set.\nfunc (r ReadonlySet) Has(addr address) bool {\n\treturn r.set.Has(addr)\n}\n\n// Size returns the number of addresses in the underlying set.\nfunc (r ReadonlySet) Size() int {\n\treturn r.set.Size()\n}\n\n// IterateByOffset walks the underlying set in sorted order, starting at\n// offset and visiting up to count addresses. fn returns true to stop early;\n// IterateByOffset returns true if iteration was stopped that way.\n//\n// The wrapped Set.IterateByOffset has no return value, so the \"stopped\"\n// result is synthesized from the last callback return via a\n// closure-captured local.\nfunc (r ReadonlySet) IterateByOffset(offset, count int, fn func(addr address) bool) (stopped bool) {\n\tr.set.IterateByOffset(offset, count, func(a address) bool {\n\t\tstopped = fn(a)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// ReverseIterateByOffset is IterateByOffset in reverse (descending) order.\nfunc (r ReadonlySet) ReverseIterateByOffset(offset, count int, fn func(addr address) bool) (stopped bool) {\n\tr.set.ReverseIterateByOffset(offset, count, func(a address) bool {\n\t\tstopped = fn(a)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"table","path":"gno.land/p/sunspirit/table","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/sunspirit/table\"\ngno = \"0.9\"\n"},{"name":"table.gno","body":"package table\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Table defines the structure for a markdown table\ntype Table struct {\n\theader []string\n\trows   [][]string\n}\n\n// Validate checks if the number of columns in each row matches the number of columns in the header\nfunc (t *Table) Validate() error {\n\tnumCols := len(t.header)\n\tfor _, row := range t.rows {\n\t\tif len(row) != numCols {\n\t\t\treturn ufmt.Errorf(\"row %v does not match header length %d\", row, numCols)\n\t\t}\n\t}\n\treturn nil\n}\n\n// New creates a new Table instance, ensuring the header and rows match in size\nfunc New(header []string, rows [][]string) (*Table, error) {\n\tt := \u0026Table{\n\t\theader: header,\n\t\trows:   rows,\n\t}\n\n\tif err := t.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn t, nil\n}\n\n// Table returns a markdown string for the given Table\nfunc (t *Table) String() string {\n\tif err := t.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"| \" + strings.Join(t.header, \" | \") + \" |\\n\")\n\tsb.WriteString(\"| \" + strings.Repeat(\"---|\", len(t.header)) + \"\\n\")\n\n\tfor _, row := range t.rows {\n\t\tsb.WriteString(\"| \" + strings.Join(row, \" | \") + \" |\\n\")\n\t}\n\n\treturn sb.String()\n}\n\n// AddRow adds a new row to the table\nfunc (t *Table) AddRow(row []string) error {\n\tif len(row) != len(t.header) {\n\t\treturn ufmt.Errorf(\"row %v does not match header length %d\", row, len(t.header))\n\t}\n\tt.rows = append(t.rows, row)\n\treturn nil\n}\n\n// AddColumn adds a new column to the table with the specified values\nfunc (t *Table) AddColumn(header string, values []string) error {\n\tif len(values) != len(t.rows) {\n\t\treturn ufmt.Errorf(\"values length %d does not match the number of rows %d\", len(values), len(t.rows))\n\t}\n\n\t// Add the new header\n\tt.header = append(t.header, header)\n\n\t// Add the new column values to each row\n\tfor i, value := range values {\n\t\tt.rows[i] = append(t.rows[i], value)\n\t}\n\treturn nil\n}\n\n// RemoveRow removes a row from the table by its index\nfunc (t *Table) RemoveRow(index int) error {\n\tif index \u003c 0 || index \u003e= len(t.rows) {\n\t\treturn ufmt.Errorf(\"index %d is out of range\", index)\n\t}\n\tt.rows = append(t.rows[:index], t.rows[index+1:]...)\n\treturn nil\n}\n\n// RemoveColumn removes a column from the table by its index\nfunc (t *Table) RemoveColumn(index int) error {\n\tif index \u003c 0 || index \u003e= len(t.header) {\n\t\treturn ufmt.Errorf(\"index %d is out of range\", index)\n\t}\n\n\t// Remove the column from the header\n\tt.header = append(t.header[:index], t.header[index+1:]...)\n\n\t// Remove the corresponding column from each row\n\tfor i := range t.rows {\n\t\tt.rows[i] = append(t.rows[i][:index], t.rows[i][index+1:]...)\n\t}\n\treturn nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"crossrealm_e","path":"gno.land/r/tests/vm/crossrealm_e","files":[{"name":"crossrealm.gno","body":"package crossrealm_e\n\nimport (\n\t\"chain/runtime/unsafe\"\n)\n\nvar (\n\tbalance int64\n\towner   address\n)\n\nfunc init() {\n\tbalance = 1000\n\tSetOwner(address(\"g1dao_address_here\"))\n}\n\n// SetOwner is an internal helper that was exported by mistake\n// (should be setOwner). Without the pre-mutation readonly check,\n// a cross-realm caller could call SetOwner + recover to silently\n// hijack ownership in memory, then call TransferToken to steal funds.\nfunc SetOwner(o address) {\n\towner = o\n}\n\nfunc GetOwner() address {\n\treturn owner\n}\n\nfunc TransferOwnership(cur realm, o address) {\n\tif unsafe.PreviousRealm().Address() != owner {\n\t\tpanic(\"unauthorized\")\n\t}\n\towner = o\n}\n\nfunc TransferToken(cur realm) {\n\tcaller := unsafe.PreviousRealm().Address()\n\tif caller != owner {\n\t\tpanic(\"unauthorized\")\n\t}\n\tbalance -= 500\n\tprintln(\"===send token to: \", caller)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/crossrealm_e\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"nestedpkg","path":"gno.land/p/demo/nestedpkg","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/nestedpkg\"\ngno = \"0.9\"\n"},{"name":"nestedpkg.gno","body":"// Package nestedpkg provides helpers for package-path based access control.\n// It is useful for upgrade patterns relying on namespaces.\n//\n// SECURITY: every exported helper takes `rlm realm` and reads both\n// `rlm.PkgPath()` and `rlm.Previous().PkgPath()` to make an\n// authorization decision. To close Class-2 designation forgery (a\n// hostile realm stashes a captured realm value and passes it back to\n// spoof identity), every helper gates on `rlm.IsCurrent()` first. The\n// Is* predicates return false on stale rlm (fail-closed); the Assert*\n// helpers panic. See docs/resources/gno-security.md.\npackage nestedpkg\n\nimport \"strings\"\n\n// IsCallerSubPath checks if the caller realm is located in a subfolder of the current realm.\nfunc IsCallerSubPath(_ int, rlm realm) bool {\n\tif !rlm.IsCurrent() {\n\t\treturn false\n\t}\n\tvar (\n\t\tcurPath  = rlm.PkgPath() + \"/\"\n\t\tprevPath = rlm.Previous().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(prevPath, curPath)\n}\n\n// AssertCallerIsSubPath panics if IsCallerSubPath returns false.\nfunc AssertCallerIsSubPath(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tvar (\n\t\tcurPath  = rlm.PkgPath() + \"/\"\n\t\tprevPath = rlm.Previous().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(prevPath, curPath) {\n\t\tpanic(\"call restricted to nested packages. current realm is \" + curPath + \", previous realm is \" + prevPath)\n\t}\n}\n\n// IsCallerParentPath checks if the caller realm is located in a parent location of the current realm.\nfunc IsCallerParentPath(_ int, rlm realm) bool {\n\tif !rlm.IsCurrent() {\n\t\treturn false\n\t}\n\tvar (\n\t\tcurPath  = rlm.PkgPath() + \"/\"\n\t\tprevPath = rlm.Previous().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(curPath, prevPath)\n}\n\n// AssertCallerIsParentPath panics if IsCallerParentPath returns false.\nfunc AssertCallerIsParentPath(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tvar (\n\t\tcurPath  = rlm.PkgPath() + \"/\"\n\t\tprevPath = rlm.Previous().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(curPath, prevPath) {\n\t\tpanic(\"call restricted to parent packages. current realm is \" + curPath + \", previous realm is \" + prevPath)\n\t}\n}\n\n// IsSameNamespace checks if the caller realm and the current realm are in the same namespace.\nfunc IsSameNamespace(_ int, rlm realm) bool {\n\tif !rlm.IsCurrent() {\n\t\treturn false\n\t}\n\tvar (\n\t\tcurNs  = nsFromPath(rlm.PkgPath()) + \"/\"\n\t\tprevNs = nsFromPath(rlm.Previous().PkgPath()) + \"/\"\n\t)\n\treturn curNs == prevNs\n}\n\n// AssertIsSameNamespace panics if IsSameNamespace returns false.\nfunc AssertIsSameNamespace(_ int, rlm realm) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tvar (\n\t\tcurNs  = nsFromPath(rlm.PkgPath()) + \"/\"\n\t\tprevNs = nsFromPath(rlm.Previous().PkgPath()) + \"/\"\n\t)\n\tif curNs != prevNs {\n\t\tpanic(\"call restricted to packages from the same namespace. current realm is \" + curNs + \", previous realm is \" + prevNs)\n\t}\n}\n\n// nsFromPath extracts the namespace from a package path.\nfunc nsFromPath(pkgpath string) string {\n\tparts := strings.Split(pkgpath, \"/\")\n\n\t// Specifically for gno.land, potential paths are in the form of DOMAIN/r/NAMESPACE/...\n\t// XXX: Consider extra checks.\n\t// XXX: Support non gno.land domains, where p/ and r/ won't be enforced.\n\tif len(parts) \u003e= 3 {\n\t\treturn parts[2]\n\t}\n\treturn \"\"\n}\n\n// XXX: Consider adding IsCallerDirectlySubPath\n// XXX: Consider adding IsCallerDirectlyParentPath\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"vm","path":"gno.land/r/tests/vm","files":[{"name":"README.md","body":"Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n"},{"name":"exploit.gno","body":"package vm\n\nvar MyFoo *Foo\n\ntype Foo struct {\n\tA int\n\tB *Foo\n}\n\n// method to mutate\n\nfunc (f *Foo) UpdateFoo(x int) {\n\tf.A = x\n}\n\nfunc init() {\n\tMyFoo = \u0026Foo{\n\t\tA: 1,\n\t\tB: \u0026Foo{\n\t\t\tA: 2,\n\t\t},\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm\"\ngno = \"0.9\"\n"},{"name":"interfaces.gno","body":"package vm\n\nimport (\n\t\"strconv\"\n)\n\ntype Stringer interface {\n\tString() string\n}\n\nvar stringers []Stringer\n\nfunc AddStringer(cur realm, str Stringer) {\n\t// NOTE: this is ridiculous, a slice that will become too long\n\t// eventually.  Don't do this in production programs; use\n\t// gno.land/p/nt/avl/v0 or similar structures.\n\tstringers = append(stringers, str)\n}\n\nfunc Render(path string) string {\n\tres := \"\"\n\t// NOTE: like the function above, this function too will eventually\n\t// become too expensive to call.\n\tfor i, stringer := range stringers {\n\t\tres += strconv.Itoa(i) + \": \" + stringer.String() + \"\\n\"\n\t}\n\treturn res\n}\n"},{"name":"realm_compositelit.gno","body":"package vm\n\ntype (\n\tWord uint\n\tnat  []Word\n)\n\nvar zero = \u0026Int{\n\tneg: true,\n\tabs: []Word{0},\n}\n\n// structLit\ntype Int struct {\n\tneg bool\n\tabs nat\n}\n\nfunc GetZeroType() nat {\n\ta := zero.abs\n\treturn a\n}\n"},{"name":"realm_method38d.gno","body":"package vm\n\nvar abs nat\n\nfunc (n nat) Add() nat {\n\treturn []Word{0}\n}\n\nfunc GetAbs(cur realm) nat {\n\tabs = []Word{0}\n\treturn abs\n}\n\nfunc AbsAdd(cur realm) nat {\n\trt := GetAbs(cur).Add()\n\treturn rt\n}\n"},{"name":"tests.gno","body":"package vm\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/demo/nestedpkg\"\n\trsubtests \"gno.land/r/tests/vm/subtests\"\n)\n\nvar counter int\n\nfunc IncCounter(cur realm) {\n\tcounter++\n}\n\nfunc Counter(cur realm) int {\n\treturn counter\n}\n\nfunc CurrentRealmPath(cur realm) string {\n\treturn unsafe.CurrentRealm().PkgPath()\n}\n\nvar initOriginCaller = unsafe.OriginCaller()\n\nfunc InitOriginCaller(cur realm) address {\n\treturn initOriginCaller\n}\n\nfunc CallAssertOriginCall(cur realm) {\n\truntime.AssertOriginCall()\n}\n\nfunc CallIsOriginCall(cur realm) bool {\n\t// XXX: consider return !unsafe.PreviousRealm().IsCode()\n\treturn unsafe.PreviousRealm().IsUser()\n}\n\nfunc CallSubtestsAssertOriginCall(cur realm) {\n\trsubtests.CallAssertOriginCall(cross(cur))\n}\n\nfunc CallSubtestsIsOriginCall(cur realm) bool {\n\treturn rsubtests.CallIsOriginCall(cross(cur))\n}\n\n//----------------------------------------\n// Test structure to ensure cross-realm modification is prevented.\n\ntype TestRealmObject struct {\n\tField string\n}\n\nvar TestRealmObjectValue TestRealmObject\n\n// NewTestRealmObject returns a fresh heap-allocated TestRealmObject.\n// Non-crossing — relies on borrow rule #1 to set m.Realm = /r/tests/vm\n// inside the body so the composite literal passes checkConstructionTime.\nfunc NewTestRealmObject() *TestRealmObject {\n\treturn \u0026TestRealmObject{Field: \"initial\"}\n}\n\nfunc ModifyTestRealmObject(cur realm, t *TestRealmObject) {\n\tt.Field += \"_modified\"\n}\n\nfunc (t *TestRealmObject) Modify() {\n\tt.Field += \"_modified\"\n}\n\n//----------------------------------------\n// Test helpers to test a particular realm bug.\n\ntype TestNode struct {\n\tName  string\n\tChild *TestNode\n}\n\nvar (\n\tgTestNode1 *TestNode\n\tgTestNode2 *TestNode\n\tgTestNode3 *TestNode\n)\n\nfunc InitTestNodes(cur realm) {\n\tgTestNode1 = \u0026TestNode{Name: \"first\"}\n\tgTestNode2 = \u0026TestNode{Name: \"second\", Child: \u0026TestNode{Name: \"second's child\"}}\n}\n\nfunc ModTestNodes(cur realm) {\n\ttmp := \u0026TestNode{}\n\ttmp.Child = gTestNode2.Child\n\tgTestNode3 = tmp // set to new-real\n\t// gTestNode1 = tmp.Child // set back to original is-real\n\tgTestNode3 = nil // delete.\n}\n\nfunc PrintTestNodes() {\n\tprintln(gTestNode2.Child.Name)\n}\n\nfunc GetPreviousRealm(cur realm) runtime.Realm {\n\treturn unsafe.PreviousRealm()\n}\n\nfunc GetRSubtestsPreviousRealm(cur realm) runtime.Realm {\n\treturn rsubtests.GetPreviousRealm(cross(cur))\n}\n\nfunc Exec(fn func()) {\n\t// no realm switching.\n\tfn()\n}\n\n// ExecRlm mirrors Exec but threads the caller's rlm into the callback\n// so the callback can use `cross(rlm)` instead of bare `cross`.\nfunc ExecRlm(_ int, rlm realm, fn func(_ int, rlm realm)) {\n\tfn(0, rlm)\n}\n\nfunc ExecSwitch(cur realm, fn func()) {\n\tfn()\n}\n\n// ExecSwitchRlm is the rlm-threaded variant of ExecSwitch — crosses\n// into this realm and passes the callee's cur to the callback so\n// the callback can `cross(rlm)` against the switched realm.\nfunc ExecSwitchRlm(cur realm, fn func(_ int, rlm realm)) {\n\tfn(0, cur)\n}\n\nfunc IsCallerSubPath(cur realm) bool {\n\treturn nestedpkg.IsCallerSubPath(0, cur)\n}\n\nfunc IsCallerParentPath(cur realm) bool {\n\treturn nestedpkg.IsCallerParentPath(0, cur)\n}\n\nfunc HasCallerSameNamespace(cur realm) bool {\n\treturn nestedpkg.IsSameNamespace(0, cur)\n}\n\nfunc BankerOriginSend(cur realm) string {\n\treturn unsafe.OriginSend().String()\n}\n\nfunc RTestsOriginSend(cur realm) string {\n\treturn rsubtests.BankerOriginSend(cross(cur))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ctg","path":"gno.land/p/leon/ctg","files":[{"name":"converter.gno","body":"// Package ctg is a simple utility package with helpers\n// for bech32 address conversions.\npackage ctg\n\nimport (\n\t\"crypto/bech32\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// ConvertCosmosToGno takes a Bech32 Cosmos address (prefix \"cosmos\")\n// and returns the same address re-encoded with the gno.land prefix \"g\".\nfunc ConvertCosmosToGno(addr string) (address, error) {\n\tprefix, decoded, err := bech32.Decode(addr)\n\tif err != nil {\n\t\treturn \"\", ufmt.Errorf(\"bech32 decode failed: %v\", err)\n\t}\n\n\tif prefix != \"cosmos\" {\n\t\treturn \"\", ufmt.Errorf(\"expected a cosmos address, got prefix %q\", prefix)\n\t}\n\n\treturn address(mustEncode(\"g\", decoded)), nil\n}\n\nfunc mustEncode(hrp string, data []byte) string {\n\tenc, err := bech32.Encode(hrp, data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn enc\n}\n\n// ConvertAnyToGno converts *any* valid Bech32 address to its gno.land form\n// by preserving the underlying payload but replacing the prefix with \"g\".\n// No prefix check is performed; invalid Bech32 input still returns an error.\nfunc ConvertAnyToGno(addr string) (address, error) {\n\t_, decoded, err := bech32.Decode(addr)\n\tif err != nil {\n\t\treturn \"\", ufmt.Errorf(\"bech32 decode failed: %v\", err)\n\t}\n\treturn address(mustEncode(\"g\", decoded)), nil\n}\n\n// ConvertGnoToAny converts a gno.land address (prefixed with \"g\") to another Bech32\n// prefix given by prefix. The function ensures the source address really\n// is a gno.land address before proceeding.\n//\n// Example:\n//\n//\tcosmosAddr, _ := ConvertGnoToAny(\"cosmos\", \"g1k98jx9...\")\n//\tfmt.Println(cosmosAddr) // → cosmos1....\nfunc ConvertGnoToAny(prefix string, addr address) (string, error) {\n\torigPrefix, decoded, err := bech32.Decode(string(addr))\n\tif err != nil {\n\t\treturn \"\", ufmt.Errorf(\"bech32 decode failed: %v\", err)\n\t}\n\tif origPrefix != \"g\" {\n\t\treturn \"\", ufmt.Errorf(\"expected a gno address but got prefix %q\", origPrefix)\n\t}\n\treturn mustEncode(prefix, decoded), nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/ctg\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"ghverify","path":"gno.land/r/gnoland/ghverify","files":[{"name":"README.md","body":"# ghverify\n\nThis realm is intended to enable off chain gno address to github handle verification.\nThe steps are as follows:\n- A user calls `RequestVerification` and provides a github handle. This creates a new static oracle feed.\n- An off-chain agent controlled by the owner of this realm requests current feeds using the `GnorkleEntrypoint` function and provides a message of `\"request\"`\n- The agent receives the task information that includes the github handle and the gno address. It performs the verification step by checking whether this github user has the address in a github repository it controls.\n- The agent publishes the result of the verification by calling `GnorkleEntrypoint` with a message structured like: `\"ingest,\u003ctask id\u003e,\u003cverification status\u003e\"`. The verification status is `OK` if verification succeeded and any other value if it failed.\n- The oracle feed's ingester processes the verification and the handle to address mapping is written to the avl trees that exist as ghverify realm variables.\n"},{"name":"contract.gno","body":"package ghverify\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\t// The agent should send this value if it has verified the github handle.\n\tverifiedResult = \"OK\"\n)\n\nvar (\n\townerAddress = unsafe.OriginCaller()\n\toracle       *gnorkle.Instance\n\tpostHandler  postGnorkleMessageHandler\n\n\thandleToAddressMap = bptree.NewBPTree32()\n\taddressToHandleMap = bptree.NewBPTree32()\n)\n\nfunc init() {\n\toracle = gnorkle.NewInstance()\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\ntype postGnorkleMessageHandler struct{}\n\n// Handle does post processing after a message is ingested by the oracle feed. It extracts the value to realm\n// storage and removes the feed from the oracle.\nfunc (h postGnorkleMessageHandler) Handle(i *gnorkle.Instance, funcType message.FuncType, feed gnorkle.Feed) error {\n\tif funcType != message.FuncTypeIngest {\n\t\treturn nil\n\t}\n\n\tresult, _, consumable := feed.Value()\n\tif !consumable {\n\t\treturn nil\n\t}\n\n\t// The value is consumable, meaning the ingestion occurred, so we can remove the feed from the oracle\n\t// after saving it to realm storage.\n\tdefer oracle.RemoveFeed(feed.ID())\n\n\t// Couldn't verify; nothing to do.\n\tif result.String != verifiedResult {\n\t\treturn nil\n\t}\n\n\tfeedTasks := feed.Tasks()\n\tif len(feedTasks) != 1 {\n\t\treturn errors.New(\"expected feed to have exactly one task\")\n\t}\n\n\ttask, ok := feedTasks[0].(*verificationTask)\n\tif !ok {\n\t\treturn errors.New(\"expected ghverify task\")\n\t}\n\n\thandleToAddressMap.Set(task.githubHandle, task.gnoAddress)\n\taddressToHandleMap.Set(task.gnoAddress, task.githubHandle)\n\treturn nil\n}\n\n// RequestVerification creates a new static feed with a single task that will\n// instruct an agent to verify the github handle / gno address pair.\nfunc RequestVerification(cur realm, githubHandle string) {\n\tgnoAddress := string(unsafe.OriginCaller())\n\tif err := oracle.AddFeeds(\n\t\tstatic.NewSingleValueFeed(\n\t\t\tgnoAddress,\n\t\t\t\"string\",\n\t\t\t\u0026verificationTask{\n\t\t\t\tgnoAddress:   gnoAddress,\n\t\t\t\tgithubHandle: githubHandle,\n\t\t\t},\n\t\t),\n\t); err != nil {\n\t\tpanic(err)\n\t}\n\tchain.Emit(\n\t\t\"verification_requested\",\n\t\t\"from\", gnoAddress,\n\t\t\"handle\", githubHandle,\n\t)\n}\n\n// GnorkleEntrypoint is the entrypoint to the gnorkle oracle handler.\nfunc GnorkleEntrypoint(cur realm, message string) string {\n\tresult, err := oracle.HandleMessage(message, postHandler)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn result\n}\n\n// SetOwner transfers ownership of the contract to the given address.\nfunc SetOwner(_ realm, owner address) {\n\tif ownerAddress != unsafe.OriginCaller() {\n\t\tpanic(\"only the owner can set a new owner\")\n\t}\n\n\townerAddress = owner\n\n\t// In the context of this contract, the owner is the only one that can\n\t// add new feeds to the oracle.\n\toracle.ClearWhitelist(\"\")\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\n// GetHandleByAddress returns the github handle associated with the given gno address.\nfunc GetHandleByAddress(cur realm, address_XXX string) string {\n\tif value := addressToHandleMap.Get(address_XXX); value != nil {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// GetAddressByHandle returns the gno address associated with the given github handle.\nfunc GetAddressByHandle(cur realm, handle string) string {\n\tif value := handleToAddressMap.Get(handle); value != nil {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// Render returns a json object string will all verified handle -\u003e address mappings.\nfunc Render(_ string) string {\n\tresult := \"{\"\n\tvar appendComma bool\n\thandleToAddressMap.Iterate(\"\", \"\", func(handle string, address_XXX any) bool {\n\t\tif appendComma {\n\t\t\tresult += \",\"\n\t\t}\n\n\t\tresult += `\"` + handle + `\": \"` + address_XXX.(string) + `\"`\n\t\tappendComma = true\n\n\t\treturn false\n\t})\n\n\treturn result + \"}\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/ghverify\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"task.gno","body":"package ghverify\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n)\n\ntype verificationTask struct {\n\tgnoAddress   string\n\tgithubHandle string\n}\n\n// MarshalJSON marshals the task contents to JSON.\nfunc (t *verificationTask) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write(\n\t\t[]byte(`{\"gno_address\":\"` + t.gnoAddress + `\",\"github_handle\":\"` + t.githubHandle + `\"}`),\n\t)\n\n\tw.Flush()\n\treturn buf.Bytes(), nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"avlhelpers","path":"gno.land/p/jefft0/avlhelpers","files":[{"name":"avlhelpers.gno","body":"package avlhelpers\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Iterate the keys in-order starting from the given prefix.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc IterateByteStringKeysByPrefix(tree avl.ITree, prefix string, cb avl.IterCbFn) {\n\tend := \"\"\n\tn := len(prefix)\n\t// To make the end of the search, increment the final character ASCII by one.\n\tfor n \u003e 0 {\n\t\tif ascii := int(prefix[n-1]); ascii \u003c 0xff {\n\t\t\tend = prefix[0:n-1] + string(ascii+1)\n\t\t\tbreak\n\t\t}\n\n\t\t// The last character is 0xff. Try the previous character.\n\t\tn--\n\t}\n\n\ttree.Iterate(prefix, end, cb)\n}\n\n// Get a list of keys starting from the given prefix. Limit the\n// number of results to maxResults.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc ListByteStringKeysByPrefix(tree avl.ITree, prefix string, maxResults int) []string {\n\tresult := []string{}\n\tIterateByteStringKeysByPrefix(tree, prefix, func(key string, value any) bool {\n\t\tresult = append(result, key)\n\t\tif len(result) \u003e= maxResults {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn result\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jefft0/avlhelpers\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"grc721","path":"gno.land/p/demo/tokens/grc721","files":[{"name":"basic_nft.gno","body":"package grc721\n\nimport (\n\t\"chain\"\n\t\"math/overflow\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype BasicNFT struct {\n\tname              string\n\tsymbol            string\n\torigRealm         string   // owning realm's package path\n\towners            avl.Tree // tokenId -\u003e OwnerAddress\n\tbalances          avl.Tree // OwnerAddress -\u003e TokenCount\n\ttokenApprovals    avl.Tree // TokenId -\u003e ApprovedAddress\n\ttokenURIs         avl.Tree // TokenId -\u003e URIs\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\nfunc NewBasicNFT(_ int, rlm realm, name, symbol string) *BasicNFT {\n\tif !rlm.IsCurrent() {\n\t\tpanic(ErrSpoofedRealm)\n\t}\n\tpkgPath := rlm.PkgPath()\n\tif pkgPath == \"\" {\n\t\tpanic(ErrNotRealm)\n\t}\n\tif !validName(name) {\n\t\tpanic(ErrInvalidName)\n\t}\n\tif !validSymbol(symbol) {\n\t\tpanic(ErrInvalidSymbol)\n\t}\n\n\treturn \u0026BasicNFT{\n\t\tname:      name,\n\t\tsymbol:    symbol,\n\t\torigRealm: pkgPath,\n\n\t\towners:            avl.Tree{},\n\t\tbalances:          avl.Tree{},\n\t\ttokenApprovals:    avl.Tree{},\n\t\ttokenURIs:         avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *BasicNFT) Name() string      { return s.name }\nfunc (s *BasicNFT) Symbol() string    { return s.symbol }\nfunc (s *BasicNFT) TokenCount() int64 { return int64(s.owners.Size()) }\n\nfunc (s *BasicNFT) ID() string {\n\treturn s.origRealm + \".\" + s.symbol\n}\n\n// BalanceOf returns balance of input address\nfunc (s *BasicNFT) BalanceOf(addr address) (int64, error) {\n\tif err := isValidAddress(addr); err != nil {\n\t\treturn 0, err\n\t}\n\n\tbalance := s.balances.Get(addr.String())\n\tif balance == nil {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(int64), nil\n}\n\n// OwnerOf returns owner of input token id\nfunc (s *BasicNFT) OwnerOf(tid TokenID) (address, error) {\n\towner := s.owners.Get(string(tid))\n\tif owner == nil {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn owner.(address), nil\n}\n\n// TokenURI returns the URI of input token id\nfunc (s *BasicNFT) TokenURI(tid TokenID) (string, error) {\n\turi := s.tokenURIs.Get(tid.String())\n\tif uri == nil {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn uri.(string), nil\n}\n\n// SetTokenURI sets the URI of a token. caller must equal the token's\n// owner. The owning realm's public wrapper is responsible for deriving\n// caller from rlm.Previous().Address() under an rlm.IsCurrent() guard\n// before invoking this method; this method trusts the supplied caller.\nfunc (s *BasicNFT) SetTokenURI(caller address, tid TokenID, tURI TokenURI) (bool, error) {\n\t// check for invalid TokenID\n\tif !s.exists(tid) {\n\t\treturn false, ErrInvalidTokenId\n\t}\n\n\t// check for the right owner\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif caller != owner {\n\t\treturn false, ErrCallerIsNotOwner\n\t}\n\ts.tokenURIs.Set(tid.String(), tURI.String())\n\n\tchain.Emit(\n\t\tTokenURIUpdateEvent,\n\t\t\"token\", s.ID(),\n\t\t\"tokenId\", tid.String(),\n\t)\n\n\treturn true, nil\n}\n\n// IsApprovedForAll returns true if operator is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *BasicNFT) IsApprovedForAll(owner, operator address) bool {\n\tkey := owner.String() + \":\" + operator.String()\n\tapproved := s.operatorApprovals.Get(key)\n\tif approved == nil {\n\t\treturn false\n\t}\n\n\treturn approved.(bool)\n}\n\n// Approve approves the input address for a particular token. caller\n// must be the owner OR an operator approved for all on owner's behalf.\n// The owning realm's wrapper validates IsCurrent and derives caller\n// from rlm.Previous().Address() before calling.\nfunc (s *BasicNFT) Approve(caller, to address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner == to {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tif caller != owner \u0026\u0026 !s.IsApprovedForAll(owner, caller) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\ttidStr := tid.String()\n\ts.tokenApprovals.Set(tidStr, to)\n\n\tchain.Emit(\n\t\tApprovalEvent,\n\t\t\"token\", s.ID(),\n\t\t\"owner\", owner.String(),\n\t\t\"to\", to.String(),\n\t\t\"tokenId\", tidStr,\n\t)\n\n\treturn nil\n}\n\n// GetApproved return the approved address for token\nfunc (s *BasicNFT) GetApproved(tid TokenID) (address, error) {\n\taddr := s.tokenApprovals.Get(tid.String())\n\tif addr == nil {\n\t\treturn zeroAddress, ErrTokenIdNotHasApproved\n\t}\n\n\treturn addr.(address), nil\n}\n\n// SetApprovalForAll grants/revokes operator permission across all of\n// the caller's tokens. caller is the owner whose approvals are mutated;\n// the owning realm's wrapper derives it from rlm.Previous().Address()\n// under an IsCurrent() guard.\nfunc (s *BasicNFT) SetApprovalForAll(caller, operator address, approved bool) error {\n\tif err := isValidAddress(operator); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// SafeTransferFrom transfers a token from `from` to `to`, checking that\n// contract recipients are aware of the GRC721 protocol to prevent\n// tokens from being forever locked. caller must be the owner or an\n// approved operator. The owning realm's wrapper derives caller from\n// rlm.Previous().Address() under an IsCurrent() guard.\nfunc (s *BasicNFT) SafeTransferFrom(caller, from, to address, tid TokenID) error {\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(from, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\n// TransferFrom transfers a token from `from` to `to`. Same caller\n// contract as SafeTransferFrom.\nfunc (s *BasicNFT) TransferFrom(caller, from, to address, tid TokenID) error {\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Mints `tokenId` and transfers it to `to`.\nfunc (s *BasicNFT) Mint(to address, tid TokenID) error {\n\treturn s.mint(to, tid)\n}\n\n// Mints `tokenId` and transfers it to `to`. Also checks that\n// contract recipients are using GRC721 protocol\nfunc (s *BasicNFT) SafeMint(to address, tid TokenID) error {\n\terr := s.mint(to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(zeroAddress, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\nfunc (s *BasicNFT) Burn(tid TokenID) error {\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.beforeTokenTransfer(owner, zeroAddress, tid, 1)\n\n\ttidStr := tid.String()\n\ts.tokenApprovals.Remove(tidStr)\n\tbalance, err := s.BalanceOf(owner)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbalance = overflow.Sub64p(balance, 1)\n\n\townerStr := owner.String()\n\ts.balances.Set(ownerStr, balance)\n\ts.owners.Remove(tidStr)\n\n\tchain.Emit(\n\t\tBurnEvent,\n\t\t\"token\", s.ID(),\n\t\t\"from\", ownerStr,\n\t\t\"tokenId\", tidStr,\n\t)\n\n\ts.afterTokenTransfer(owner, zeroAddress, tid, 1)\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll()\nfunc (s *BasicNFT) setApprovalForAll(owner, operator address, approved bool) error {\n\tif owner == operator {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\ts.operatorApprovals.Set(key, approved)\n\n\tchain.Emit(\n\t\tApprovalForAllEvent,\n\t\t\"token\", s.ID(),\n\t\t\"owner\", owner.String(),\n\t\t\"to\", operator.String(),\n\t\t\"approved\", strconv.FormatBool(approved),\n\t)\n\n\treturn nil\n}\n\n// Helper for TransferFrom() and SafeTransferFrom()\nfunc (s *BasicNFT) transfer(from, to address, tid TokenID) error {\n\tif err := isValidAddress(from); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\tif err := isValidAddress(to); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.beforeTokenTransfer(from, to, tid, 1)\n\n\t// Check that tokenId was not transferred by `beforeTokenTransfer`\n\towner, err = s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ttidStr := tid.String()\n\ts.tokenApprovals.Remove(tidStr)\n\tfromBalance, err := s.BalanceOf(from)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfromBalance = overflow.Sub64p(fromBalance, 1)\n\ttoBalance = overflow.Add64p(toBalance, 1)\n\n\tfromStr := from.String()\n\ttoStr := to.String()\n\n\ts.balances.Set(fromStr, fromBalance)\n\ts.balances.Set(toStr, toBalance)\n\ts.owners.Set(tidStr, to)\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", s.ID(),\n\t\t\"from\", fromStr,\n\t\t\"to\", toStr,\n\t\t\"tokenId\", tidStr,\n\t)\n\n\ts.afterTokenTransfer(from, to, tid, 1)\n\n\treturn nil\n}\n\n// Helper for Mint() and SafeMint()\nfunc (s *BasicNFT) mint(to address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ts.beforeTokenTransfer(zeroAddress, to, tid, 1)\n\n\t// Check that tokenId was not minted by `beforeTokenTransfer`\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance = overflow.Add64p(toBalance, 1)\n\ttoStr := to.String()\n\ttidStr := tid.String()\n\ts.balances.Set(toStr, toBalance)\n\ts.owners.Set(tidStr, to)\n\n\tchain.Emit(\n\t\tMintEvent,\n\t\t\"token\", s.ID(),\n\t\t\"to\", toStr,\n\t\t\"tokenId\", tidStr,\n\t)\n\n\ts.afterTokenTransfer(zeroAddress, to, tid, 1)\n\n\treturn nil\n}\n\nfunc (s *BasicNFT) isApprovedOrOwner(addr address, tid TokenID) bool {\n\towner := s.owners.Get(tid.String())\n\tif owner == nil {\n\t\treturn false\n\t}\n\n\townerAddr := owner.(address)\n\tif addr == ownerAddr || s.IsApprovedForAll(ownerAddr, addr) {\n\t\treturn true\n\t}\n\n\tapproved, err := s.GetApproved(tid)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn approved == addr\n}\n\n// Checks if token id already exists\nfunc (s *BasicNFT) exists(tid TokenID) bool {\n\treturn s.owners.Has(tid.String())\n}\n\nfunc (s *BasicNFT) beforeTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {\n\t// TODO: Implementation\n}\n\nfunc (s *BasicNFT) afterTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {\n\t// TODO: Implementation\n}\n\nfunc (s *BasicNFT) checkOnGRC721Received(from, to address, tid TokenID) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *BasicNFT) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", s.name, s.symbol)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", s.TokenCount())\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", s.balances.Size())\n\n\treturn\n}\n\n// Getter returns an NFTGetter that yields a reader-only view of the NFT.\n// Safe to register with cross-realm aggregators like tokenhub — readers\n// only, no rlm-typed methods, so no cur can be captured via this surface.\nfunc (n *BasicNFT) Getter() NFTGetter {\n\treturn func() IGRC721Reader {\n\t\treturn n\n\t}\n}\n"},{"name":"errors.gno","body":"package grc721\n\nimport \"errors\"\n\nvar (\n\tErrInvalidTokenId              = errors.New(\"invalid token id\")\n\tErrInvalidAddress              = errors.New(\"invalid address\")\n\tErrTokenIdNotHasApproved       = errors.New(\"token id not approved for anyone\")\n\tErrApprovalToCurrentOwner      = errors.New(\"approval to current owner\")\n\tErrCallerIsNotOwner            = errors.New(\"caller is not token owner\")\n\tErrCallerNotApprovedForAll     = errors.New(\"caller is not approved for all\")\n\tErrCannotTransferToSelf        = errors.New(\"cannot send transfer to self\")\n\tErrTransferFromIncorrectOwner  = errors.New(\"transfer from incorrect owner\")\n\tErrTransferToNonGRC721Receiver = errors.New(\"transfer to non GRC721Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved  = errors.New(\"caller is not token owner or approved\")\n\tErrTokenIdAlreadyExists        = errors.New(\"token id already exists\")\n\n\t// NewBasicNFT realm binding\n\tErrSpoofedRealm  = errors.New(\"rlm does not match the current crossing frame\")\n\tErrNotRealm      = errors.New(\"rlm must be a realm (got EOA/origin)\")\n\tErrInvalidName   = errors.New(\"invalid token name (empty, too long, or contains control chars)\")\n\tErrInvalidSymbol = errors.New(\"invalid token symbol (empty, too long, or contains chars outside [A-Za-z0-9_-])\")\n\n\t// ERC721Royalty\n\tErrInvalidRoyaltyPercentage     = errors.New(\"invalid royalty percentage\")\n\tErrInvalidRoyaltyPaymentAddress = errors.New(\"invalid royalty paymentAddress\")\n\tErrCannotCalculateRoyaltyAmount = errors.New(\"cannot calculate royalty amount\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tokens/grc721\"\ngno = \"0.9\"\n"},{"name":"grc721_metadata.gno","body":"package grc721\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// metadataNFT represents an NFT with metadata extensions.\ntype metadataNFT struct {\n\t*BasicNFT\n\textensions *avl.Tree // AVL tree for storing metadata extensions\n}\n\n// Ensure that metadataNFT implements the IGRC721MetadataOnchain interface.\nvar _ IGRC721MetadataOnchain = (*metadataNFT)(nil)\n\n// NewNFTWithMetadata creates a new basic NFT with metadata extensions.\nfunc NewNFTWithMetadata(_ int, rlm realm, name, symbol string) *metadataNFT {\n\treturn \u0026metadataNFT{\n\t\tBasicNFT:   NewBasicNFT(0, rlm, name, symbol),\n\t\textensions: avl.NewTree(),\n\t}\n}\n\n// SetTokenMetadata sets metadata for a given token ID. The token must exist and\n// caller must equal its owner; the owning realm's wrapper derives caller from\n// rlm.Previous().Address() under IsCurrent (mirrors SetTokenRoyalty).\nfunc (s *metadataNFT) SetTokenMetadata(caller address, tid TokenID, metadata Metadata) error {\n\t// Check that the token exists and the caller is its owner.\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set the metadata for the token ID in the extensions AVL tree\n\ts.extensions.Set(tid.String(), metadata)\n\n\tchain.Emit(\n\t\tMetadataUpdateEvent,\n\t\t\"token\", s.ID(),\n\t\t\"tokenId\", tid.String(),\n\t)\n\n\treturn nil\n}\n\n// TokenMetadata retrieves metadata for a given token ID.\nfunc (s *metadataNFT) TokenMetadata(tid TokenID) (Metadata, error) {\n\t// Retrieve metadata from the extensions AVL tree\n\tmetadata := s.extensions.Get(tid.String())\n\tif metadata == nil {\n\t\treturn Metadata{}, ErrInvalidTokenId\n\t}\n\n\treturn metadata.(Metadata), nil\n}\n\n// Basic NFT methods forwarded to embedded BasicNFT\n\nfunc (s *metadataNFT) Name() string {\n\treturn s.BasicNFT.Name()\n}\n\nfunc (s *metadataNFT) Symbol() string {\n\treturn s.BasicNFT.Symbol()\n}\n\nfunc (s *metadataNFT) TokenCount() int64 {\n\treturn s.BasicNFT.TokenCount()\n}\n\nfunc (s *metadataNFT) BalanceOf(addr address) (int64, error) {\n\treturn s.BasicNFT.BalanceOf(addr)\n}\n\nfunc (s *metadataNFT) OwnerOf(tid TokenID) (address, error) {\n\treturn s.BasicNFT.OwnerOf(tid)\n}\n\nfunc (s *metadataNFT) TokenURI(tid TokenID) (string, error) {\n\treturn s.BasicNFT.TokenURI(tid)\n}\n\nfunc (s *metadataNFT) SetTokenURI(caller address, tid TokenID, tURI TokenURI) (bool, error) {\n\treturn s.BasicNFT.SetTokenURI(caller, tid, tURI)\n}\n\nfunc (s *metadataNFT) IsApprovedForAll(owner, operator address) bool {\n\treturn s.BasicNFT.IsApprovedForAll(owner, operator)\n}\n\nfunc (s *metadataNFT) Approve(caller, to address, tid TokenID) error {\n\treturn s.BasicNFT.Approve(caller, to, tid)\n}\n\nfunc (s *metadataNFT) GetApproved(tid TokenID) (address, error) {\n\treturn s.BasicNFT.GetApproved(tid)\n}\n\nfunc (s *metadataNFT) SetApprovalForAll(caller, operator address, approved bool) error {\n\treturn s.BasicNFT.SetApprovalForAll(caller, operator, approved)\n}\n\nfunc (s *metadataNFT) SafeTransferFrom(caller, from, to address, tid TokenID) error {\n\treturn s.BasicNFT.SafeTransferFrom(caller, from, to, tid)\n}\n\nfunc (s *metadataNFT) TransferFrom(caller, from, to address, tid TokenID) error {\n\treturn s.BasicNFT.TransferFrom(caller, from, to, tid)\n}\n\nfunc (s *metadataNFT) Mint(to address, tid TokenID) error {\n\treturn s.BasicNFT.Mint(to, tid)\n}\n\nfunc (s *metadataNFT) SafeMint(to address, tid TokenID) error {\n\treturn s.BasicNFT.SafeMint(to, tid)\n}\n\nfunc (s *metadataNFT) Burn(tid TokenID) error {\n\treturn s.BasicNFT.Burn(tid)\n}\n\nfunc (s *metadataNFT) RenderHome() string {\n\treturn s.BasicNFT.RenderHome()\n}\n"},{"name":"grc721_royalty.gno","body":"package grc721\n\nimport (\n\t\"math/overflow\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// royaltyNFT represents a non-fungible token (NFT) with royalty functionality.\ntype royaltyNFT struct {\n\t*metadataNFT                   // Embedding metadataNFT for NFT functionality\n\ttokenRoyaltyInfo     *avl.Tree // AVL tree to store royalty information for each token\n\tmaxRoyaltyPercentage int64     // maxRoyaltyPercentage represents the maximum royalty percentage that can be charged every sale\n}\n\n// Ensure that royaltyNFT implements the IGRC2981 interface.\nvar _ IGRC2981 = (*royaltyNFT)(nil)\n\n// NewNFTWithRoyalty creates a new royalty NFT with the specified name, symbol, and royalty calculator.\nfunc NewNFTWithRoyalty(_ int, rlm realm, name, symbol string) *royaltyNFT {\n\treturn \u0026royaltyNFT{\n\t\tmetadataNFT:          NewNFTWithMetadata(0, rlm, name, symbol),\n\t\ttokenRoyaltyInfo:     avl.NewTree(),\n\t\tmaxRoyaltyPercentage: 100,\n\t}\n}\n\n// SetTokenRoyalty sets the royalty information for a specific token ID.\n// caller must equal the token's owner; the owning realm's wrapper\n// derives caller from rlm.Previous().Address() under IsCurrent.\nfunc (r *royaltyNFT) SetTokenRoyalty(caller address, tid TokenID, royaltyInfo RoyaltyInfo) error {\n\t// Validate the payment address\n\tif err := isValidAddress(royaltyInfo.PaymentAddress); err != nil {\n\t\treturn ErrInvalidRoyaltyPaymentAddress\n\t}\n\n\t// Check if royalty percentage exceeds maxRoyaltyPercentage\n\tif royaltyInfo.Percentage \u003e r.maxRoyaltyPercentage {\n\t\treturn ErrInvalidRoyaltyPercentage\n\t}\n\n\t// Check if the caller is the owner of the token\n\towner, err := r.metadataNFT.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set royalty information for the token\n\tr.tokenRoyaltyInfo.Set(tid.String(), royaltyInfo)\n\n\treturn nil\n}\n\n// RoyaltyInfo returns the royalty information for the given token ID and sale price.\nfunc (r *royaltyNFT) RoyaltyInfo(tid TokenID, salePrice int64) (address, int64, error) {\n\t// Retrieve royalty information for the token\n\tval := r.tokenRoyaltyInfo.Get(tid.String())\n\tif val == nil {\n\t\treturn \"\", 0, ErrInvalidTokenId\n\t}\n\n\troyaltyInfo := val.(RoyaltyInfo)\n\n\t// Calculate royalty amount\n\troyaltyAmount, _ := r.calculateRoyaltyAmount(salePrice, royaltyInfo.Percentage)\n\n\treturn royaltyInfo.PaymentAddress, royaltyAmount, nil\n}\n\nfunc (r *royaltyNFT) calculateRoyaltyAmount(salePrice, percentage int64) (int64, error) {\n\troyaltyAmount := overflow.Mul64p(salePrice, percentage) / 100\n\treturn royaltyAmount, nil\n}\n"},{"name":"igrc721.gno","body":"package grc721\n\n// IGRC721Reader is the read-only view of an NFT. Safe to receive across\n// realm boundaries — has no rlm-typed methods, so a malicious impl can\n// only lie about read results (data-integrity issue), not capture cur.\n//\n// Writes are concrete methods on *BasicNFT / *metadataNFT / *royaltyNFT\n// only — there is no IGRC721 writer interface. The owning realm should\n// hold the concrete *BasicNFT in an unexported package var and expose\n// public rlm-validating wrappers like:\n//\n//\tfunc TransferFrom(cur realm, from, to address, tid TokenID) error {\n//\t\tcaller := cur.Previous().Address()\n//\t\treturn nft.TransferFrom(caller, from, to, tid)\n//\t}\n//\n// This is the Reader/Writer split — stronger than the Authority-pattern\n// because the writer interface doesn't exist at all, so no realm author\n// can accidentally expose it. See `r/demo/foo721` for the canonical\n// wrapper pattern.\ntype IGRC721Reader interface {\n\tName() string\n\tSymbol() string\n\tTokenCount() int64\n\tBalanceOf(owner address) (int64, error)\n\tOwnerOf(tid TokenID) (address, error)\n\tGetApproved(tid TokenID) (address, error)\n\tIsApprovedForAll(owner, operator address) bool\n}\n\ntype (\n\tTokenID  string\n\tTokenURI string\n)\n\nfunc (t TokenID) String() string  { return string(t) }\nfunc (t TokenURI) String() string { return string(t) }\n\nconst (\n\tMintEvent           = \"Mint\"\n\tBurnEvent           = \"Burn\"\n\tTransferEvent       = \"Transfer\"\n\tApprovalEvent       = \"Approval\"\n\tApprovalForAllEvent = \"ApprovalForAll\"\n\tTokenURIUpdateEvent = \"TokenUriUpdate\"\n\tMetadataUpdateEvent = \"MetadataUpdate\"\n)\n\n// NFTGetter returns a reader-only view of an NFT. Aggregators (such as\n// tokenhub) register and dispatch NFTGetters; the reader-only return\n// type means even a malicious aggregator can't be used to leak cur.\ntype NFTGetter func() IGRC721Reader\n"},{"name":"igrc721_metadata.gno","body":"package grc721\n\n// IGRC721CollectionMetadata describes basic information about an NFT collection.\ntype IGRC721CollectionMetadata interface {\n\tName() string   // Name returns the name of the collection.\n\tSymbol() string // Symbol returns the symbol of the collection.\n}\n\n// IGRC721Metadata follows the Ethereum standard\ntype IGRC721Metadata interface {\n\tIGRC721CollectionMetadata\n\tTokenURI(tid TokenID) (string, error) // TokenURI returns the URI of a specific token.\n}\n\n// IGRC721Metadata follows the OpenSea metadata standard\ntype IGRC721MetadataOnchain interface {\n\tIGRC721CollectionMetadata\n\tTokenMetadata(tid TokenID) (Metadata, error)\n}\n\ntype Trait struct {\n\tDisplayType string\n\tTraitType   string\n\tValue       string\n}\n\n// see: https://docs.opensea.io/docs/metadata-standards\ntype Metadata struct {\n\tImage           string  // URL to the image of the item. Can be any type of image (including SVGs, which will be cached into PNGs by OpenSea), IPFS or Arweave URLs or paths. We recommend using a minimum 3000 x 3000 image.\n\tImageData       string  // Raw SVG image data, if you want to generate images on the fly (not recommended). Only use this if you're not including the image parameter.\n\tExternalURL     string  // URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site.\n\tDescription     string  // Human-readable description of the item. Markdown is supported.\n\tName            string  // Name of the item.\n\tAttributes      []Trait // Attributes for the item, which will show up on the OpenSea page for the item.\n\tBackgroundColor string  // Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #\n\tAnimationURL    string  // URL to a multimedia attachment for the item. Supported file extensions: GLTF, GLB, WEBM, MP4, M4V, OGV, OGG, MP3, WAV, OGA, HTML (for rich experiences and interactive NFTs using JavaScript canvas, WebGL, etc.). Scripts and relative paths within the HTML page are now supported. Access to browser extensions is not supported.\n\tYoutubeURL      string  // URL to a YouTube video (only used if animation_url is not provided).\n}\n"},{"name":"igrc721_royalty.gno","body":"package grc721\n\n// IGRC2981 follows the Ethereum standard\ntype IGRC2981 interface {\n\t// RoyaltyInfo retrieves royalty information for a tokenID and salePrice.\n\t// It returns the payment address, royalty amount, and an error if any.\n\tRoyaltyInfo(tokenID TokenID, salePrice int64) (address, int64, error)\n}\n\n// RoyaltyInfo represents royalty information for a token.\ntype RoyaltyInfo struct {\n\tPaymentAddress address // PaymentAddress is the address where royalty payment should be sent.\n\tPercentage     int64   // Percentage is the royalty percentage. It indicates the percentage of royalty to be paid for each sale. For example : Percentage = 10 =\u003e 10%\n}\n"},{"name":"util.gno","body":"package grc721\n\nconst (\n\tMaxNameLen   = 64\n\tMaxSymbolLen = 11\n)\n\nvar zeroAddress = address(\"\")\n\nfunc isValidAddress(addr address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\treturn nil\n}\n\nfunc validName(name string) bool {\n\tif name == \"\" || len(name) \u003e MaxNameLen {\n\t\treturn false\n\t}\n\tfor _, c := range name {\n\t\tif c \u003c 0x20 || c == 0x7f {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// symbol is emitted as part of the event identifier, so bounding it\n// keeps events under MaxEventAttrLen.\nfunc validSymbol(symbol string) bool {\n\tif symbol == \"\" || len(symbol) \u003e MaxSymbolLen {\n\t\treturn false\n\t}\n\tfor _, c := range symbol {\n\t\tif !isAlnum(c) \u0026\u0026 c != '_' \u0026\u0026 c != '-' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isAlnum(c rune) bool {\n\treturn (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') || (c \u003e= '0' \u0026\u0026 c \u003c= '9')\n}\n\nfunc emit(event any) {\n\t// TODO: setup a pubsub system here?\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"atomicswap","path":"gno.land/r/demo/defi/atomicswap","files":[{"name":"atomicswap.gno","body":"// Package atomicswap implements a hash time-locked contract (HTLC) for atomic swaps\n// between native coins (ugnot) or GRC20 tokens.\n//\n// An atomic swap allows two parties to exchange assets in a trustless way, where\n// either both transfers happen or neither does. The process works as follows:\n//\n//  1. Alice wants to swap with Bob. She generates a secret and creates a swap with\n//     Bob's address and the hash of the secret (hashlock).\n//\n//  2. Bob can claim the assets by providing the correct secret before the timelock expires.\n//     The secret proves Bob knows the preimage of the hashlock.\n//\n// 3. If Bob doesn't claim in time, Alice can refund the assets back to herself.\n//\n// Example usage for native coins:\n//\n//\t// Alice creates a swap with 1000ugnot for Bob\n//\tsecret := \"mysecret\"\n//\thashlock := hex.EncodeToString(sha256.Sum256([]byte(secret)))\n//\tid, _ := atomicswap.NewCoinSwap(bobAddr, hashlock) // -send 1000ugnot\n//\n//\t// Bob claims the swap by providing the secret\n//\tatomicswap.Claim(id, \"mysecret\")\n//\n// Example usage for GRC20 tokens:\n//\n//\t// Alice approves the swap contract to spend her tokens\n//\ttoken.Approve(swapAddr, 1000)\n//\n//\t// Alice creates a swap with 1000 tokens for Bob\n//\tid, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, \"gno.land/r/demo/token.TKN\")\n//\n//\t// Bob claims the swap by providing the secret\n//\tatomicswap.Claim(id, \"mysecret\")\n//\n// If Bob doesn't claim in time (default 1 week), Alice can refund:\n//\n//\tatomicswap.Refund(id)\npackage atomicswap\n\nimport (\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nconst defaultTimelockDuration = 7 * 24 * time.Hour // 1w\n\nvar (\n\tswaps   avl.Tree // id -\u003e *Swap\n\tcounter int\n)\n\n// NewCoinSwap creates a new atomic swap contract for native coins.\n// It uses a default timelock duration.\nfunc NewCoinSwap(cur realm, recipient address, hashlock string) (int, *Swap) {\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\treturn NewCustomCoinSwap(cur, recipient, hashlock, timelock)\n}\n\n// NewGRC20Swap creates a new atomic swap contract for grc20 tokens.\n// It uses gno.land/r/demo/defi/grc20reg to lookup for a registered token.\nfunc NewGRC20Swap(cur realm, recipient address, hashlock string, tokenRegistryKey string) (int, *Swap) {\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\ttoken := grc20reg.MustGet(tokenRegistryKey)\n\treturn NewCustomGRC20Swap(cur, recipient, hashlock, timelock, token)\n}\n\n// NewCoinSwapWithTimelock creates a new atomic swap contract for native coin.\n// It allows specifying a custom timelock duration.\n//\n// Only direct user-call (maketx call) is accepted: unsafe.OriginSend()\n// describes a real receipt at this realm only when the caller is a pure\n// EOA. Intermediate code realms or `maketx run` ephemeral realms can\n// attach -send to the tx but spend the envelope elsewhere, leaving\n// OriginSend() describing a phantom payment that would let the swap\n// drain the realm's pre-existing balance on Claim.\nfunc NewCustomCoinSwap(cur realm, recipient address, hashlock string, timelock time.Time) (int, *Swap) {\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\tsender := cur.Previous().Address()\n\tsent := unsafe.OriginSend()\n\trequire(len(sent) != 0, \"at least one coin needs to be sent\")\n\n\t// Create the swap\n\tsendFn := func(cur realm, to address) {\n\t\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\t\tpkgAddr := cur.Address()\n\t\tbanker_.SendCoins(pkgAddr, to, sent)\n\t}\n\tamountStr := sent.String()\n\tswap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)\n\n\tcounter++\n\tid := strconv.Itoa(counter)\n\tswaps.Set(id, swap)\n\treturn counter, swap\n}\n\n// NewCustomGRC20Swap creates a new atomic swap contract for grc20 tokens.\n// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.\nfunc NewCustomGRC20Swap(cur realm, recipient address, hashlock string, timelock time.Time, token *grc20.Token) (int, *Swap) {\n\tsender := cur.Previous().Address()\n\tcurAddr := cur.Address()\n\n\tallowance := token.Allowance(sender, curAddr)\n\trequire(allowance \u003e 0, \"no allowance\")\n\n\tuserTeller := token.RealmTeller(0, cur)\n\terr := userTeller.TransferFrom(0, cur, sender, curAddr, allowance)\n\trequire(err == nil, \"cannot retrieve tokens from allowance\")\n\n\tamountStr := ufmt.Sprintf(\"%d%s\", allowance, token.GetSymbol())\n\tsendFn := func(cur realm, to address) {\n\t\terr := userTeller.Transfer(0, cur, to, allowance)\n\t\trequire(err == nil, \"cannot transfer tokens\")\n\t}\n\n\tswap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)\n\n\tcounter++\n\tid := strconv.Itoa(counter)\n\tswaps.Set(id, swap)\n\n\treturn counter, swap\n}\n\n// Claim loads a registered swap and tries to claim it.\nfunc Claim(cur realm, id int, secret string) {\n\tswap := mustGet(id)\n\tswap.Claim(0, cur, secret)\n}\n\n// Refund loads a registered swap and tries to refund it.\nfunc Refund(cur realm, id int) {\n\tswap := mustGet(id)\n\tswap.Refund(0, cur)\n}\n\n// Render returns a list of swaps (simplified) for the homepage, and swap details when specifying a swap ID.\nfunc Render(path string) string {\n\tif path == \"\" { // home\n\t\toutput := \"\"\n\t\tsize := swaps.Size()\n\t\tmax := 10\n\t\tswaps.ReverseIterateByOffset(size-max, max, func(key string, value any) bool {\n\t\t\tswap := value.(*Swap)\n\t\t\toutput += ufmt.Sprintf(\"- %s: %s -(%s)\u003e %s - %s\\n\",\n\t\t\t\tkey, swap.sender, swap.amountStr, swap.recipient, swap.Status())\n\t\t\treturn false\n\t\t})\n\t\treturn output\n\t} else { // by id\n\t\tswap := swaps.Get(path)\n\t\tif swap == nil {\n\t\t\treturn \"404\"\n\t\t}\n\t\treturn swap.(*Swap).String()\n\t}\n}\n\n// require checks a condition and panics with a message if the condition is false.\nfunc require(check bool, msg string) {\n\tif !check {\n\t\tpanic(msg)\n\t}\n}\n\n// mustGet retrieves a swap by its id or panics.\nfunc mustGet(id int) *Swap {\n\tkey := strconv.Itoa(id)\n\tswap := swaps.Get(key)\n\tif swap == nil {\n\t\tpanic(\"unknown swap ID\")\n\t}\n\treturn swap.(*Swap)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/atomicswap\"\ngno = \"0.9\"\n"},{"name":"swap.gno","body":"package atomicswap\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"time\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Swap represents an atomic swap contract.\ntype Swap struct {\n\tsender    address\n\trecipient address\n\thashlock  string\n\ttimelock  time.Time\n\tclaimed   bool\n\trefunded  bool\n\tamountStr string\n\tsendFn    func(cur realm, to address)\n}\n\nfunc newSwap(\n\tsender address,\n\trecipient address,\n\thashlock string,\n\ttimelock time.Time,\n\tamountStr string,\n\tsendFn func(realm, address),\n) *Swap {\n\trequire(time.Now().Before(timelock), \"timelock must be in the future\")\n\trequire(hashlock != \"\", \"hashlock must not be empty\")\n\treturn \u0026Swap{\n\t\trecipient: recipient,\n\t\tsender:    sender,\n\t\thashlock:  hashlock,\n\t\ttimelock:  timelock,\n\t\tclaimed:   false,\n\t\trefunded:  false,\n\t\tsendFn:    sendFn,\n\t\tamountStr: amountStr,\n\t}\n}\n\n// Claim allows the recipient to claim the funds if they provide the correct preimage.\n// rlm is the cur of the surrounding crossing wrapper; rlm.Previous() is\n// the immediate caller of that wrapper, against which we authorize.\nfunc (s *Swap) Claim(_ int, rlm realm, preimage string) {\n\trequire(rlm.IsCurrent(), \"unauthorized: rlm is not the caller's live cur\")\n\trequire(!s.claimed, \"already claimed\")\n\trequire(!s.refunded, \"already refunded\")\n\trequire(rlm.Previous().Address() == s.recipient, \"unauthorized\")\n\n\thashlock := sha256.Sum256([]byte(preimage))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\trequire(hashlockHex == s.hashlock, \"invalid preimage\")\n\n\ts.claimed = true\n\ts.sendFn(cross(rlm), s.recipient)\n}\n\n// Refund allows the sender to refund the funds after the timelock has expired.\nfunc (s *Swap) Refund(_ int, rlm realm) {\n\trequire(rlm.IsCurrent(), \"unauthorized: rlm is not the caller's live cur\")\n\trequire(!s.claimed, \"already claimed\")\n\trequire(!s.refunded, \"already refunded\")\n\trequire(rlm.Previous().Address() == s.sender, \"unauthorized\")\n\trequire(time.Now().After(s.timelock), \"timelock not expired\")\n\n\ts.refunded = true\n\ts.sendFn(cross(rlm), s.sender)\n}\n\nfunc (s Swap) Status() string {\n\tswitch {\n\tcase s.refunded:\n\t\treturn \"refunded\"\n\tcase s.claimed:\n\t\treturn \"claimed\"\n\tcase s.TimeRemaining() \u003c 0:\n\t\treturn \"expired\"\n\tdefault:\n\t\treturn \"active\"\n\t}\n}\n\nfunc (s Swap) TimeRemaining() time.Duration {\n\tremaining := time.Until(s.timelock)\n\tif remaining \u003c 0 {\n\t\treturn 0\n\t}\n\treturn remaining\n}\n\n// String returns the current state of the swap.\nfunc (s Swap) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"- status: %s\\n- sender: %s\\n- recipient: %s\\n- amount: %s\\n- hashlock: %s\\n- timelock: %s\\n- remaining: %s\",\n\t\ts.Status(), s.sender, s.recipient, s.amountStr, s.hashlock, s.timelock.Format(time.RFC3339), s.TimeRemaining().String(),\n\t)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"foo20","path":"gno.land/r/demo/defi/foo20","files":[{"name":"foo20.gno","body":"// foo20 is a GRC20 token contract where all the grc20.Teller methods are\n// proxified with top-level functions. see also gno.land/r/demo/bar20.\npackage foo20\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nvar (\n\tToken         *grc20.Token\n\tprivateLedger *grc20.PrivateLedger\n\tuserTeller    grc20.Teller\n\tOwnable       = ownable.NewWithAddress(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\") // govdao t1 multisig\n)\n\nfunc init(cur realm) {\n\t// foo20 only ever creates this one token, so id 0 can't collide.\n\tToken, privateLedger = grc20.NewToken(\"Foo\", \"FOO\", 4, 0, cur)\n\tuserTeller = Token.CallerTeller()\n\tprivateLedger.Mint(Ownable.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M)\n\tgrc20reg.Register(cross(cur), Token, \"\")\n}\n\nfunc TotalSupply() int64 {\n\treturn userTeller.TotalSupply()\n}\n\nfunc BalanceOf(owner address) int64 {\n\treturn userTeller.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender address) int64 {\n\treturn userTeller.Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, to address, amount int64) {\n\tcheckErr(userTeller.Transfer(0, cur, to, amount))\n}\n\nfunc Approve(cur realm, spender address, amount int64) {\n\tcheckErr(userTeller.Approve(0, cur, spender, amount))\n}\n\nfunc TransferFrom(cur realm, from, to address, amount int64) {\n\tcheckErr(userTeller.TransferFrom(0, cur, from, to, amount))\n}\n\n// Faucet is distributing foo20 tokens without restriction (unsafe).\n// For a real token faucet, you should take care of setting limits are asking payment.\nfunc Faucet(cur realm) {\n\tcaller := cur.Previous().Address()\n\tamount := int64(1_000 * 10_000) // 1k\n\tcheckErr(privateLedger.Mint(caller, amount))\n}\n\nfunc Mint(cur realm, to address, amount int64) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tcheckErr(privateLedger.Mint(to, amount))\n}\n\nfunc Burn(cur realm, from address, amount int64) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\tcheckErr(privateLedger.Burn(from, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := address(parts[1])\n\t\tbalance := userTeller.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/defi/foo20\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"disperse","path":"gno.land/r/demo/disperse","files":[{"name":"disperse.gno","body":"package disperse\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\n\ttokens \"gno.land/r/demo/defi/grc20factory\"\n)\n\n// DisperseUgnot parses receivers and amounts and sends out ugnot\n// The function will send out the coins to the addresses and return the leftover coins to the caller\n// if there are any to return\nfunc DisperseUgnot(cur realm, addresses []address, coins chain.Coins) {\n\t// Reject non-EOA callers: unsafe.OriginSend() and the realm-balance\n\t// check below describe coins that actually landed at this realm only\n\t// when the caller is a pure EOA. A `maketx run` ephemeral realm or\n\t// intermediate code realm could otherwise consume the envelope and\n\t// have this function disperse pre-existing realm balance to\n\t// attacker-chosen addresses.\n\tif !cur.Previous().IsUserCall() {\n\t\tpanic(\"only user-call (maketx call) accepted\")\n\t}\n\tcoinSent := unsafe.OriginSend()\n\tcaller := cur.Previous().Address()\n\trealmAddr := cur.Address()\n\tbanker_ := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\n\tif len(addresses) != len(coins) {\n\t\tpanic(ErrNumAddrValMismatch)\n\t}\n\n\tfor _, coin := range coins {\n\t\tif coin.Amount \u003c= 0 {\n\t\t\tpanic(ErrNegativeCoinAmount)\n\t\t}\n\n\t\tif banker_.GetCoin(realmAddr, coin.Denom) \u003c coin.Amount {\n\t\t\tpanic(ErrMismatchBetweenSentAndParams)\n\t\t}\n\t}\n\n\t// Send coins\n\tfor i := range addresses {\n\t\tbanker_.SendCoins(realmAddr, addresses[i], chain.NewCoins(coins[i]))\n\t}\n\n\t// Return possible leftover coins\n\tfor _, coin := range coinSent {\n\t\tleftoverAmt := banker_.GetCoin(realmAddr, coin.Denom)\n\t\tif leftoverAmt \u003e 0 {\n\t\t\tsend := chain.Coins{chain.NewCoin(coin.Denom, leftoverAmt)}\n\t\t\tbanker_.SendCoins(realmAddr, caller, send)\n\t\t}\n\t}\n}\n\n// DisperseUgnotString receives a string of addresses and a string of amounts\n// and parses them to be used in DisperseUgnot\nfunc DisperseUgnotString(cur realm, addresses string, amounts string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, err := parseAmounts(amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcoins := make(chain.Coins, len(parsedAmounts))\n\tfor i, amount := range parsedAmounts {\n\t\tcoins[i] = chain.NewCoin(\"ugnot\", amount)\n\t}\n\n\tDisperseUgnot(cur, parsedAddresses, coins)\n}\n\n// DisperseGRC20 disperses tokens to multiple addresses\n// Note that it is necessary to approve the realm to spend the tokens before calling this function\n// see the corresponding filetests for examples\nfunc DisperseGRC20(cur realm, addresses []address, amounts []int64, symbols []string) {\n\tcaller := cur.Previous().Address()\n\n\tif (len(addresses) != len(amounts)) || (len(amounts) != len(symbols)) {\n\t\tpanic(ErrArgLenAndSentLenMismatch)\n\t}\n\tfor _, amount := range amounts {\n\t\tif amount \u003c 0 {\n\t\t\tpanic(ErrInvalidAmount)\n\t\t}\n\t}\n\n\tfor i := 0; i \u003c len(addresses); i++ {\n\t\ttokens.TransferFrom(cross(cur), symbols[i], caller, addresses[i], amounts[i])\n\t}\n}\n\n// DisperseGRC20String receives a string of addresses and a string of tokens\n// and parses them to be used in DisperseGRC20\nfunc DisperseGRC20String(cur realm, addresses string, tokens string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, parsedSymbols, err := parseTokens(tokens)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tDisperseGRC20(cur, parsedAddresses, parsedAmounts, parsedSymbols)\n}\n"},{"name":"doc.gno","body":"// Package disperse provides methods to disperse coins or GRC20 tokens among multiple addresses.\n//\n// The disperse package is an implementation of an existing service that allows users to send coins or GRC20 tokens to multiple addresses\n// on the Ethereum blockchain.\n//\n// Usage:\n// To use disperse, you can either use `DisperseUgnot` to send coins or `DisperseGRC20` to send GRC20 tokens to multiple addresses.\n//\n// Example:\n// Dispersing 200 coins to two addresses:\n// - DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n// Dispersing 200 worth of a GRC20 token \"TEST\" to two addresses:\n// - DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n//\n// Reference:\n// - [the original dispere app](https://disperse.app/)\n// - [the original disperse app on etherscan](https://etherscan.io/address/0xd152f549545093347a162dce210e7293f1452150#code)\n// - [the gno disperse web app](https://gno-disperse.netlify.app/)\npackage disperse // import \"gno.land/r/demo/disperse\"\n"},{"name":"errors.gno","body":"package disperse\n\nimport \"errors\"\n\nvar (\n\tErrNotEnoughCoin                = errors.New(\"disperse: not enough coin sent in\")\n\tErrNumAddrValMismatch           = errors.New(\"disperse: number of addresses and values to send doesn't match\")\n\tErrInvalidAddress               = errors.New(\"disperse: invalid address\")\n\tErrNegativeCoinAmount           = errors.New(\"disperse: coin amount cannot be negative\")\n\tErrMismatchBetweenSentAndParams = errors.New(\"disperse: mismatch between coins sent and params called\")\n\tErrArgLenAndSentLenMismatch     = errors.New(\"disperse: mismatch between coins sent and args called\")\n\tErrInvalidAmount                = errors.New(\"disperse: invalid amount\")\n)\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/disperse\"\ngno = \"0.9\"\n"},{"name":"util.gno","body":"package disperse\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nfunc parseAddresses(addresses string) ([]address, error) {\n\tvar ret []address\n\n\tfor _, str := range strings.Split(addresses, \",\") {\n\t\taddr := address(str)\n\t\tif !addr.IsValid() {\n\t\t\treturn nil, ErrInvalidAddress\n\t\t}\n\n\t\tret = append(ret, addr)\n\t}\n\n\treturn ret, nil\n}\n\nfunc splitString(input string) (string, string) {\n\tvar pos int\n\tfor i, char := range input {\n\t\tif !unicode.IsDigit(char) {\n\t\t\tpos = i\n\t\t\tbreak\n\t\t}\n\t}\n\treturn input[:pos], input[pos:]\n}\n\nfunc parseTokens(tokens string) ([]int64, []string, error) {\n\tvar amounts []int64\n\tvar symbols []string\n\n\tfor _, token := range strings.Split(tokens, \",\") {\n\t\tamountStr, symbol := splitString(token)\n\t\tamount, _ := strconv.Atoi(amountStr)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tamounts = append(amounts, int64(amount))\n\t\tsymbols = append(symbols, symbol)\n\t}\n\n\treturn amounts, symbols, nil\n}\n\nfunc parseAmounts(amounts string) ([]int64, error) {\n\tvar ret []int64\n\n\tfor _, amt := range strings.Split(amounts, \",\") {\n\t\tamount, _ := strconv.Atoi(amt)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tret = append(ret, int64(amount))\n\t}\n\n\treturn ret, nil\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"events","path":"gno.land/r/devrels/events","files":[{"name":"errors.gno","body":"package events\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n)\n\nvar (\n\tErrEmptyName                 = errors.New(\"event name cannot be empty\")\n\tErrNoSuchID                  = errors.New(\"event with specified ID does not exist\")\n\tErrMinWidgetSize             = errors.New(\"you need to request at least 1 event to render\")\n\tErrMaxWidgetSize             = errors.New(\"maximum number of events in widget is\" + strconv.Itoa(MaxWidgetSize))\n\tErrDescriptionTooLong        = errors.New(\"event description is too long\")\n\tErrInvalidStartTime          = errors.New(\"invalid start time format\")\n\tErrInvalidEndTime            = errors.New(\"invalid end time format\")\n\tErrEndBeforeStart            = errors.New(\"end time cannot be before start time\")\n\tErrStartEndTimezonemMismatch = errors.New(\"start and end timezones are not the same\")\n)\n"},{"name":"events.gno","body":"// Package events allows you to upload data about specific IRL/online events\n// It includes dynamic support for updating rendering events based on their\n// status, ie if they are upcoming, in progress, or in the past.\npackage events\n\nimport (\n\t\"chain\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype (\n\tEvent struct {\n\t\tid          string\n\t\tname        string    // name of event\n\t\tdescription string    // short description of event\n\t\tlink        string    // link to auth corresponding web2 page, ie eventbrite/luma or conference page\n\t\tlocation    string    // location of the event\n\t\tstartTime   time.Time // given in RFC3339\n\t\tendTime     time.Time // end time of the event, given in RFC3339\n\t}\n\n\teventsSlice []*Event\n)\n\nvar (\n\tOwnable   = ownable.NewWithAddress(address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")) // @leohhhn\n\tevents    = make(eventsSlice, 0)                                                        // sorted\n\tidCounter seqid.ID\n)\n\nconst (\n\tmaxDescLength = 100\n\tEventAdded    = \"EventAdded\"\n\tEventDeleted  = \"EventDeleted\"\n\tEventEdited   = \"EventEdited\"\n)\n\n// AddEvent adds auth new event\n// Start time \u0026 end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00\nfunc AddEvent(cur realm, name, description, link, location, startTime, endTime string) (string, error) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\n\tif strings.TrimSpace(name) == \"\" {\n\t\treturn \"\", ErrEmptyName\n\t}\n\n\tif len(description) \u003e maxDescLength {\n\t\treturn \"\", ufmt.Errorf(\"%s: provided length is %d, maximum is %d\", ErrDescriptionTooLong, len(description), maxDescLength)\n\t}\n\n\t// Parse times\n\tst, et, err := parseTimes(startTime, endTime)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid := idCounter.Next().String()\n\te := \u0026Event{\n\t\tid:          id,\n\t\tname:        name,\n\t\tdescription: description,\n\t\tlink:        link,\n\t\tlocation:    location,\n\t\tstartTime:   st,\n\t\tendTime:     et,\n\t}\n\n\tevents = append(events, e)\n\tsort.Sort(events)\n\n\tchain.Emit(EventAdded,\n\t\t\"id\", e.id,\n\t)\n\n\treturn id, nil\n}\n\n// DeleteEvent deletes an event with auth given ID\nfunc DeleteEvent(cur realm, id string) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\n\te, idx, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tevents = append(events[:idx], events[idx+1:]...)\n\n\tchain.Emit(EventDeleted,\n\t\t\"id\", e.id,\n\t)\n}\n\n// EditEvent edits an event with auth given ID\n// It only updates values corresponding to non-empty arguments sent with the call\n// Note: if you need to update the start time or end time, you need to provide both every time\nfunc EditEvent(cur realm, id string, name, description, link, location, startTime, endTime string) {\n\tOwnable.AssertOwnedBy(cur.Previous().Address())\n\n\te, _, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Set only valid values\n\tif strings.TrimSpace(name) != \"\" {\n\t\te.name = name\n\t}\n\n\tif strings.TrimSpace(description) != \"\" {\n\t\te.description = description\n\t}\n\n\tif strings.TrimSpace(link) != \"\" {\n\t\te.link = link\n\t}\n\n\tif strings.TrimSpace(location) != \"\" {\n\t\te.location = location\n\t}\n\n\tif strings.TrimSpace(startTime) != \"\" || strings.TrimSpace(endTime) != \"\" {\n\t\tst, et, err := parseTimes(startTime, endTime)\n\t\tif err != nil {\n\t\t\tpanic(err) // need to also revert other state changes\n\t\t}\n\n\t\toldStartTime := e.startTime\n\t\te.startTime = st\n\t\te.endTime = et\n\n\t\t// If sort order was disrupted, sort again\n\t\tif oldStartTime != e.startTime {\n\t\t\tsort.Sort(events)\n\t\t}\n\t}\n\n\tchain.Emit(EventEdited,\n\t\t\"id\", e.id,\n\t)\n}\n\nfunc GetEventByID(id string) (*Event, int, error) {\n\tfor i, event := range events {\n\t\tif event.id == id {\n\t\t\treturn event, i, nil\n\t\t}\n\t}\n\n\treturn nil, -1, ErrNoSuchID\n}\n\n// Len returns the length of the slice\nfunc (m eventsSlice) Len() int {\n\treturn len(m)\n}\n\n// Less compares the startTime fields of two elements\n// In this case, events will be sorted by largest startTime first (upcoming \u003e past)\nfunc (m eventsSlice) Less(i, j int) bool {\n\treturn m[i].startTime.After(m[j].startTime)\n}\n\n// Swap swaps two elements in the slice\nfunc (m eventsSlice) Swap(i, j int) {\n\tm[i], m[j] = m[j], m[i]\n}\n\n// parseTimes parses the start and end time for an event and checks for possible errors\nfunc parseTimes(startTime, endTime string) (time.Time, time.Time, error) {\n\tst, err := time.Parse(time.RFC3339, startTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidStartTime, err.Error())\n\t}\n\n\tet, err := time.Parse(time.RFC3339, endTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidEndTime, err.Error())\n\t}\n\n\tif et.Before(st) {\n\t\treturn time.Time{}, time.Time{}, ErrEndBeforeStart\n\t}\n\n\t_, stOffset := st.Zone()\n\t_, etOffset := et.Zone()\n\tif stOffset != etOffset {\n\t\treturn time.Time{}, time.Time{}, ErrStartEndTimezonemMismatch\n\t}\n\n\treturn st, et, nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/devrels/events\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"render.gno","body":"package events\n\nimport (\n\t\"bytes\"\n\n\t\"time\"\n\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tMaxWidgetSize = 5\n)\n\n// RenderEventWidget shows up to eventsToRender of the latest events to a caller\nfunc RenderEventWidget(eventsToRender int) (string, error) {\n\tnumOfEvents := len(events)\n\tif numOfEvents == 0 {\n\t\treturn \"No events.\", nil\n\t}\n\n\tif eventsToRender \u003e MaxWidgetSize {\n\t\treturn \"\", ErrMaxWidgetSize\n\t}\n\n\tif eventsToRender \u003c 1 {\n\t\treturn \"\", ErrMinWidgetSize\n\t}\n\n\tif eventsToRender \u003e numOfEvents {\n\t\teventsToRender = numOfEvents\n\t}\n\n\toutput := \"\"\n\n\tfor _, event := range events[:eventsToRender] {\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", event.name, event.link)\n\t}\n\n\treturn output, nil\n}\n\n// renderHome renders the home page of the events realm\nfunc renderHome(admin bool) string {\n\toutput := \"# gno.land events\\n\\n\"\n\n\tif len(events) == 0 {\n\t\toutput += \"No upcoming or past events.\"\n\t\treturn output\n\t}\n\n\toutput += \"Below is a list of all gno.land events, including in progress, upcoming, and past ones.\\n\\n\"\n\toutput += \"---\\n\\n\"\n\n\tvar (\n\t\tinProgress []string\n\t\tupcoming   []string\n\t\tpast       []string\n\t\tnow        = time.Now()\n\t)\n\n\tfor _, e := range events {\n\t\tif now.Before(e.startTime) {\n\t\t\tupcoming = append(upcoming, e.Render(admin))\n\t\t} else if now.After(e.endTime) {\n\t\t\tpast = append(past, e.Render(admin))\n\t\t} else {\n\t\t\tinProgress = append(inProgress, e.Render(admin))\n\t\t}\n\t}\n\n\tif len(upcoming) != 0 {\n\t\t// Add upcoming events\n\t\toutput += \"## Upcoming events\\n\\n\"\n\t\toutput += md.ColumnsN(upcoming, 3, true)\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif len(inProgress) != 0 {\n\t\toutput += \"## Currently in progress\\n\\n\"\n\t\toutput += md.ColumnsN(inProgress, 3, true)\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif len(past) != 0 {\n\t\t// Add past events\n\t\toutput += \"## Past events\\n\\n\"\n\t\toutput += md.ColumnsN(past, 3, true)\n\t}\n\n\treturn output\n}\n\n// Render returns the markdown representation of a single event instance\nfunc (e Event) Render(admin bool) string {\n\tvar buf bytes.Buffer\n\n\tbuf.WriteString(ufmt.Sprintf(\"### %s\\n\\n\", e.name))\n\tbuf.WriteString(ufmt.Sprintf(\"%s\\n\\n\", e.description))\n\tbuf.WriteString(ufmt.Sprintf(\"**Location:** %s\\n\\n\", e.location))\n\n\t_, offset := e.startTime.Zone() // offset is in seconds\n\thoursOffset := offset / (60 * 60)\n\tsign := \"\"\n\tif offset \u003e= 0 {\n\t\tsign = \"+\"\n\t}\n\n\tbuf.WriteString(ufmt.Sprintf(\"**Starts:** %s UTC%s%d\\n\\n\", e.startTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\tbuf.WriteString(ufmt.Sprintf(\"**Ends:** %s UTC%s%d\\n\\n\", e.endTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\n\tif admin {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[EDIT](/r/devrels/events$help\u0026func=EditEvent\u0026id=%s)\\n\\n\", e.id))\n\t\tbuf.WriteString(ufmt.Sprintf(\"[DELETE](/r/devrels/events$help\u0026func=DeleteEvent\u0026id=%s)\\n\\n\", e.id))\n\t}\n\n\tif e.link != \"\" {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[See more](%s)\\n\\n\", e.link))\n\t}\n\n\treturn buf.String()\n}\n\n// Render is the main rendering entry point\nfunc Render(path string) string {\n\tif path == \"admin\" {\n\t\treturn renderHome(true)\n\t}\n\n\treturn renderHome(false)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"coinsort","path":"gno.land/p/leon/coinsort","files":[{"name":"coinsort.gno","body":"// Package coinsort provides helpers to sort a slice of banker.Coins using the\n// classic sort.Sort API (without relying on sort.Slice).\n//\n// Usage examples:\n//\n//\tcoins := banker.GetCoins(\"g1....\")\n//\n//\t// Ascending by balance\n//\tcoinsort.SortByBalance(coins)\n//\n//\t// Custom order – largest balance first\n//\tcoinsort.SortBy(coins, func(a, b chain.Coin) bool {\n//\t    return a.Amount \u003e b.Amount // descending\n//\t})\n//\n// Note: when getting banker.Coins from the banker, it's sorted by denom by default.\npackage coinsort\n\nimport (\n\t\"chain\"\n\t\"sort\"\n)\n\ntype ByAmount struct{ chain.Coins }\n\nfunc (b ByAmount) Len() int           { return len(b.Coins) }\nfunc (b ByAmount) Swap(i, j int)      { b.Coins[i], b.Coins[j] = b.Coins[j], b.Coins[i] }\nfunc (b ByAmount) Less(i, j int) bool { return b.Coins[i].Amount \u003c b.Coins[j].Amount }\n\n// SortByBalance sorts c in ascending order by Amount.\n//\n//\tcoinsort.SortByBalance(myCoins)\nfunc SortByBalance(c chain.Coins) {\n\tsort.Sort(ByAmount{c})\n}\n\n// LessFunc defines the comparison function for SortBy. It must return true if\n// 'a' should come before 'b'.\n\ntype LessFunc func(a, b chain.Coin) bool\n\n// customSorter adapts a LessFunc to sort.Interface so we can keep using\n// sort.Sort (rather than sort.Slice).\n\ntype customSorter struct {\n\tcoins chain.Coins\n\tless  LessFunc\n}\n\nfunc (cs customSorter) Len() int      { return len(cs.coins) }\nfunc (cs customSorter) Swap(i, j int) { cs.coins[i], cs.coins[j] = cs.coins[j], cs.coins[i] }\nfunc (cs customSorter) Less(i, j int) bool {\n\treturn cs.less(cs.coins[i], cs.coins[j])\n}\n\n// SortBy sorts c in place using the provided LessFunc.\n//\n// Example – descending by Amount:\n//\n//\tcoinsort.SortBy(coins, func(a, b banker.Coin) bool {\n//\t    return a.Amount \u003e b.Amount\n//\t})\nfunc SortBy(c chain.Coins, less LessFunc) {\n\tif less == nil {\n\t\treturn // nothing to do; keep original order\n\t}\n\tsort.Sort(customSorter{coins: c, less: less})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/coinsort\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"coins","path":"gno.land/r/gnoland/coins","files":[{"name":"coins.gno","body":"// Package coins provides simple helpers to retrieve information about coins\n// on the Gno.land blockchain.\n//\n// The primary goal of this realm is to allow users to check their token balances without\n// relying on external tools or services. This is particularly valuable for new networks\n// that aren't yet widely supported by public explorers or wallets. By using this realm,\n// users can always access their balance information directly through the gnodev.\n//\n// While currently focused on basic balance checking functionality, this realm could\n// potentially be extended to support other banker-related workflows in the future.\n// However, we aim to keep it minimal and focused on its core purpose.\n//\n// This is a \"Render-only realm\" - it exposes only a Render function as its public\n// interface and doesn't maintain any state of its own. This pattern allows for\n// simple, stateless information retrieval directly through the blockchain's\n// rendering capabilities.\npackage coins\n\nimport (\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/leon/coinsort\"\n\t\"gno.land/p/leon/ctg\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nvar router *mux.Router\n\nfunc init() {\n\trouter = mux.NewRouter()\n\n\trouter.HandleFunc(\"\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(renderHomepage())\n\t})\n\n\trouter.HandleFunc(\"balances\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(renderBalances(req))\n\t})\n\n\trouter.HandleFunc(\"convert/{address}\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(renderConvertedAddress(req.GetVar(\"address\")))\n\t})\n\n\t// Coin info\n\trouter.HandleFunc(\"supply/{denom}\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\t// banker := banker.NewReadonlyBanker()\n\t\t// res.Write(renderAddressBalance(banker, denom, denom))\n\t\tres.Write(\"The total supply feature is coming soon.\")\n\t})\n\n\trouter.NotFoundHandler = func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(\"# 404\\n\\nThat page was not found. Would you like to [**go home**?](/r/gnoland/coins)\")\n\t}\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n\nfunc renderHomepage() string {\n\treturn strings.Replace(`# Gno.land Coins Explorer\n\nThis is a simple, readonly realm that allows users to browse native coin balances. Check your coin balance below!\n\n\u003cgno-form path=\"balances\"\u003e\n\t\u003cgno-input name=\"address\" type=\"text\" placeholder=\"Valid bech32 address (e.g. g1..., cosmos1..., osmo1...)\" /\u003e\n\t\u003cgno-input name=\"coin\" type=\"text\" placeholder=\"Coin (e.g. ugnot)\"\" /\u003e\n\u003c/gno-form\u003e\n\nHere are a few more ways to use this app:\n\n- ~/r/gnoland/coins:balances?address=g1...~ - show full list of coin balances of an address\n\t- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)\n- ~/r/gnoland/coins:balances?address=g1...\u0026coin=ugnot~ - shows the balance of an address for a specific coin\n\t- [Example](/r/gnoland/coins:balances?address=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\u0026coin=ugnot)\n- ~/r/gnoland/coins:convert/\u003cbech32_addr\u003e~ - convert a bech32 address to a Gno address\n\t- [Example](/r/gnoland/coins:convert/cosmos1jg8mtutu9khhfwc4nxmuhcpftf0pajdh6svrgs)\n- ~/r/gnoland/coins:supply/\u003cdenom\u003e~ - shows the total supply of denom\n\t- Coming soon!\n\n`, \"~\", \"`\", -1)\n}\n\nfunc renderBalances(req *mux.Request) string {\n\tout := \"# Balances\\n\\n\"\n\n\tinput := req.Query.Get(\"address\")\n\tcoin := req.Query.Get(\"coin\")\n\n\tif input == \"\" \u0026\u0026 coin == \"\" {\n\t\tout += \"Please input a valid address and coin denomination.\\n\\n\"\n\t\treturn out\n\t}\n\n\tif input == \"\" {\n\t\tout += \"Please input a valid bech32 address.\\n\\n\"\n\t\treturn out\n\t}\n\n\toriginalInput := input\n\tvar wasConverted bool\n\n\t// Try to validate or convert\n\tif !address(input).IsValid() {\n\t\taddr, err := ctg.ConvertAnyToGno(input)\n\t\tif err != nil {\n\t\t\treturn out + ufmt.Sprintf(\"Tried converting `%s` to a Gno address but failed. Please try with a valid bech32 address.\\n\\n\", input)\n\t\t}\n\t\tinput = addr.String()\n\t\twasConverted = true\n\t}\n\n\tif wasConverted {\n\t\tout += ufmt.Sprintf(\"\u003e [!NOTE]\\n\u003e  Automatically converted `%s` to its Gno equivalent.\\n\\n\", originalInput)\n\t}\n\n\tbanker_ := banker.NewReadonlyBanker()\n\tbalances := banker_.GetCoins(address(input))\n\n\tif len(balances) == 0 {\n\t\tout += \"This address currently has no coins.\"\n\t\treturn out\n\t}\n\n\tif coin != \"\" {\n\t\treturn renderSingleCoinBalance(coin, input, originalInput, wasConverted, balances.AmountOf(coin))\n\t}\n\n\tuser, _ := users.ResolveAny(input)\n\tname := \"`\" + input + \"`\"\n\tif user != nil {\n\t\tname = user.RenderLink(\"\")\n\t}\n\n\tout += ufmt.Sprintf(\"This page shows full coin balances of %s at block #%d\\n\\n\",\n\t\tname, runtime.ChainHeight())\n\n\t// Determine sorting\n\tif getSortField(req) == \"balance\" {\n\t\tcoinsort.SortByBalance(balances)\n\t}\n\n\t// Create table\n\tdenomColumn := renderSortLink(req, \"denom\", \"Denomination\")\n\tbalanceColumn := renderSortLink(req, \"balance\", \"Balance\")\n\ttable := mdtable.Table{\n\t\tHeaders: []string{denomColumn, balanceColumn},\n\t}\n\n\tif isSortReversed(req) {\n\t\tfor _, b := range balances {\n\t\t\ttable.Append([]string{b.Denom, strconv.Itoa(int(b.Amount))})\n\t\t}\n\t} else {\n\t\tfor i := len(balances) - 1; i \u003e= 0; i-- {\n\t\t\ttable.Append([]string{balances[i].Denom, strconv.Itoa(int(balances[i].Amount))})\n\t\t}\n\t}\n\n\tout += table.String() + \"\\n\\n\"\n\treturn out\n}\n\n// amount is taken from the balances the caller already read, rather than read again.\n// Beyond saving the read, it keeps an unvalidated denom out of the banker: denom is\n// the \"coin\" query parameter, and GetCoin panics on a malformed one, so on a render\n// path any URL could otherwise break the page.\nfunc renderSingleCoinBalance(denom, addr, origInput string, wasConverted bool, amount int64) string {\n\tout := \"# Coin balance\\n\\n\"\n\n\tif wasConverted {\n\t\tout += ufmt.Sprintf(\"\u003e [!NOTE]\\n\u003e  Automatically converted `%s` to its Gno equivalent.\\n\\n\", origInput)\n\t}\n\n\tuser, _ := users.ResolveAny(addr)\n\tname := \"`\" + addr + \"`\"\n\tif user != nil {\n\t\tname = user.RenderLink(\"\")\n\t}\n\n\tout += ufmt.Sprintf(\"%s has `%d%s` at block #%d\\n\\n\",\n\t\tname, amount, denom, runtime.ChainHeight())\n\n\tout += \"[View full balance list for this address](/r/gnoland/coins:balances?address=\" + addr + \")\"\n\n\treturn out\n}\n\nfunc renderConvertedAddress(addr string) string {\n\tout := \"# Address converter\\n\\n\"\n\n\tgnoAddress, err := ctg.ConvertAnyToGno(addr)\n\tif err != nil {\n\t\tout += err.Error()\n\t\treturn out\n\t}\n\n\tuser, _ := users.ResolveAny(gnoAddress.String())\n\tname := \"`\" + gnoAddress.String() + \"`\"\n\tif user != nil {\n\t\tname = user.RenderLink(\"\")\n\t}\n\n\tout += ufmt.Sprintf(\"`%s` on Cosmos matches %s on gno.land.\\n\\n\", addr, name)\n\tout += \"[[View `ugnot` balance for this address]](/r/gnoland/coins:balances?address=\" + gnoAddress.String() + \"\u0026coin=ugnot) - \"\n\tout += \"[[View full balance list for this address]](/r/gnoland/coins:balances?address=\" + gnoAddress.String() + \")\"\n\treturn out\n}\n\n// Helper functions for sorting and pagination\nfunc getSortField(req *mux.Request) string {\n\tfield := req.Query.Get(\"sort\")\n\tswitch field {\n\tcase \"denom\", \"balance\":\n\t\treturn field\n\t}\n\treturn \"denom\"\n}\n\nfunc isSortReversed(req *mux.Request) bool {\n\treturn req.Query.Get(\"order\") != \"asc\"\n}\n\nfunc renderSortLink(req *mux.Request, field, label string) string {\n\tcurrentField := getSortField(req)\n\tcurrentOrder := req.Query.Get(\"order\")\n\n\tnewOrder := \"desc\"\n\tif field == currentField \u0026\u0026 currentOrder != \"asc\" {\n\t\tnewOrder = \"asc\"\n\t}\n\n\tquery := make(url.Values)\n\tfor k, vs := range req.Query {\n\t\tquery[k] = append([]string(nil), vs...)\n\t}\n\n\tquery.Set(\"sort\", field)\n\tquery.Set(\"order\", newOrder)\n\n\tif field == currentField {\n\t\tif currentOrder == \"asc\" {\n\t\t\tlabel += \" ↑\"\n\t\t} else {\n\t\t\tlabel += \" ↓\"\n\t\t}\n\t}\n\n\treturn md.Link(label, \"?\"+query.Encode())\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/coins\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"groups","path":"gno.land/p/nt/groups/v0","files":[{"name":"README.md","body":"# groups\n\nA `Group` is a set of addresses (the **base set**) plus any number of named\n**Roles**, each with its own member set and optional metadata. One `Group`\nper DAO, per board, per permissions instance — whatever your realm manages.\n\n```\nGroup\n├── base set:        the plain members (guests, users, council — you decide)\n└── roles\n    ├── \"admin\":     member set + meta\n    └── \"moderator\": member set + meta\n```\n\n## Quick start\n\n```go\nimport \"gno.land/p/nt/groups/v0\"\n\nvar group = groups.NewGroup()\n\nfunc init() {\n    // Base members.\n    group.Add(address(\"g1alice...\"))\n    group.Add(address(\"g1bob...\"))\n\n    // A role with its own members.\n    admins, _ := group.AddRole(\"admin\")\n    admins.Members().Add(address(\"g1carol...\"))\n}\n```\n\n## Three kinds of operations\n\nEvery membership operation belongs to exactly one family, so a call site\nalways says which semantic it means — checking the base set and checking\n\"anywhere in the group\" are different questions with different methods.\n\n| Family | Methods | Looks at |\n|---|---|---|\n| Base set | `Add`, `Remove`, `Has`, `Size`, `Iterate` | base set only |\n| Role registry | `AddRole`, `GetRole`, `HasRole`, `RemoveRole`, `RoleCount`, `IterateRoles` | the named roles |\n| Aggregated | `HasAny`, `TotalSize`, `IterateAll`, `RemoveFromAll` | base + every role, deduplicated |\n| Aggregated | `RolesContaining` | every role — base membership is not a role |\n\n(`NewGroup` and the `Readonly()` views sit outside the families; views are\ncovered below.)\n\nSo with alice in the base set only and dave in the \"council\" role only:\n\n```go\ngroup.Has(alice)  // true  — alice is a base member\ngroup.Has(dave)   // false — Has never consults roles\ngroup.HasAny(dave) // true — dave is somewhere in the group\ngroup.RolesContaining(dave) // [\"council\"]\n```\n\nAn address may appear in the base set and several roles at once;\n`TotalSize` and `IterateAll` count and yield it once. All iterators take\n`offset, count` for pagination, and the callback returns `true` to stop.\n\n`RemoveRole` discards only the role itself — its members stay wherever\nelse they appear. `RemoveFromAll` is the opposite: it purges one address\nfrom the base set and every role.\n\n## Sharing across realms: readonly views\n\nA `*Group` or `*Role` is a **mutable handle**: anyone holding it can change\nyour data (method calls run with the allocating realm's storage authority).\n`Readonly()` returns a view that structurally cannot mutate — no mutator\nmethods exist on it at all.\n\nThree rules at realm boundaries:\n\n1. **Never accept** a `*Group`/`*Role` from an untrusted caller.\n2. **Never return** a `*Group`/`*Role` to one — return\n   `group.Readonly()` (a `*ReadonlyGroup`) or `role.Readonly()` instead.\n3. **Never trust** a readonly view someone else hands you: it is a live\n   window onto *their* data, which they can change between your reads.\n\n## The `meta` slot\n\n`Role.SetMeta(meta any)` stores arbitrary per-role data — permission bits,\na description, a quorum. Store **value types only** (strings, ints, value\nstructs/slices). Do not store pointers to types with mutator methods (such\nas `*avl.Tree` or `*addrset.Set`): `Meta()` returns the value as-is, so a\nreader holding a readonly view could call those mutators on it.\n\nSee `doc.gno` for the precise security model, and\n`filetests/z_readme_filetest.gno` for this README as a running example.\n"},{"name":"doc.gno","body":"// Package groups provides Groups containing a base address set plus named\n// Roles, each with their own member set and metadata.\n//\n// A Group is the top-level container — one per DAO, one per permissions\n// instance, etc. A Role is a named subset within a Group with arbitrary\n// per-role metadata.\n//\n// The API separates three concerns explicitly, so each call site picks the\n// right semantic:\n//\n//   - base-only operations: Add, Remove, Has, Size, Iterate;\n//   - role registry operations: AddRole, GetRole, HasRole, RemoveRole,\n//     RoleCount, IterateRoles;\n//   - aggregated operations across base + all roles: HasAny, TotalSize,\n//     IterateAll, RolesContaining, RemoveFromAll.\n//\n// # Security model\n//\n// A Group, and the *Role values it hands out, are meant to be allocated and\n// held by the consuming realm. Three rules apply at realm boundaries:\n//\n//  1. Do not ACCEPT a *Group or *Role from an external/untrusted caller —\n//     subsequent mutations would route to the allocating (attacker)\n//     realm's authority, and a poisoned Group could cause DoS or\n//     unexpected state.\n//\n//  2. Do not RETURN a *Group or *Role from any method or function callable\n//     by untrusted realms. Return *ReadonlyGroup or *ReadonlyRole instead.\n//     Exposing a mutable handle is exactly as dangerous as accepting one.\n//\n//  3. Do not TRUST a *ReadonlyGroup or *ReadonlyRole received from an\n//     untrusted caller. A readonly view is a live handle over its creator's\n//     data, not a snapshot: the sender controls the contents and can mutate\n//     them between reads. Base authorization and accounting decisions only\n//     on views derived from a Group you allocated yourself.\n//\n// The Readonly() views are the only safe handles to cross a realm boundary —\n// safe to hand out, per rule 3 not blindly safe to consume.\n//\n// # Metadata: do not store mutable pointers\n//\n// Each Role has a free-form \"meta any\" slot. Meta() returns the stored\n// value as-is, so a pointer stored in meta can be retrieved by an untrusted\n// reader holding a Readonly() view. A direct field write through that\n// pointer is still blocked by the realm-ownership\n// gate, but invoking a MUTATOR METHOD on it (or passing it into a function\n// that mutates by argument) runs under whatever realm allocated it (borrow\n// rule #2) and commits the write. This includes common /p/ types such as\n// *addrset.Set and *avl.Tree — they are mutable pointers, not \"just data\".\n// Therefore store only:\n//\n//   - value types (ints, strings, value structs/slices with NO internal\n//     pointer reaching a mutator-bearing type), or\n//   - a wrapper whose only exported methods are read-only and which holds no\n//     externally-mutable pointer.\n//\n// # Readonly views\n//\n// Group and Role each expose a Readonly() method returning a typed\n// read-only view (ReadonlyGroup, ReadonlyRole; role member sets surface as\n// *addrset.ReadonlySet). The views are concrete structs with unexported\n// fields and only read-side exported methods, so cross-package callers\n// cannot mutate through them.\npackage groups\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/groups/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"},{"name":"group.gno","body":"package groups\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nvar (\n\tErrRoleExists = errors.New(\"role already exists\")\n\tErrEmptyName  = errors.New(\"role name is required\")\n)\n\n// Group is a container with a base address set plus a registry of named\n// Roles. The zero value is not usable; construct with NewGroup.\n//\n// # Security\n//\n// A Group, and the *Role values it hands out, are meant to be allocated and\n// held by the consuming realm. Three rules apply at realm boundaries:\n//\n//  1. Do not ACCEPT a *Group or *Role from an external/untrusted caller —\n//     subsequent mutations would route to the allocating (attacker)\n//     realm's authority, and a poisoned Group could cause DoS or\n//     unexpected state.\n//\n//  2. Do not RETURN a *Group or *Role from any method or function callable\n//     by untrusted realms. Return *ReadonlyGroup or *ReadonlyRole instead.\n//\n//  3. Do not TRUST a *ReadonlyGroup or *ReadonlyRole received from an\n//     untrusted caller — it is a live handle over the sender's data, not a\n//     snapshot; the contents are attacker-controlled and can change between\n//     reads.\n//\n// Both directions matter: exposing a *Group to attacker code is as\n// dangerous as accepting one. The Readonly() views are the only safe\n// handles to cross a realm boundary.\ntype Group struct {\n\tbase  *addrset.Set\n\troles *bptree.BPTree // name -\u003e *Role\n}\n\n// NewGroup constructs an empty group.\nfunc NewGroup() *Group {\n\treturn \u0026Group{\n\t\tbase:  \u0026addrset.Set{},\n\t\troles: bptree.NewBPTree32(),\n\t}\n}\n\n// --- Base set ---\n//\n// All base methods operate ONLY on the base set; roles are never consulted.\n// Use the aggregated forms (HasAny, TotalSize, IterateAll, RemoveFromAll)\n// for views across base + roles.\n\n// Add inserts addr into the base set. Returns true if newly added.\nfunc (g *Group) Add(addr address) (added bool) {\n\treturn g.base.Add(addr)\n}\n\n// Remove deletes addr from the base set. Returns true if it was present.\nfunc (g *Group) Remove(addr address) (removed bool) {\n\treturn g.base.Remove(addr)\n}\n\n// Has reports whether addr is in the base set. It does NOT consult roles;\n// use HasAny for an aggregated check.\nfunc (g *Group) Has(addr address) bool {\n\treturn g.base.Has(addr)\n}\n\n// Size returns the number of addresses in the base set only.\nfunc (g *Group) Size() int {\n\treturn g.base.Size()\n}\n\n// Iterate walks the base set (only) in sorted order, starting at offset.\n// fn returns true to stop; Iterate returns true if stopped early.\nfunc (g *Group) Iterate(offset, count int, fn func(addr address) bool) (stopped bool) {\n\tg.base.IterateByOffset(offset, count, func(a address) bool {\n\t\tstopped = fn(a)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// --- Role registry ---\n\n// AddRole registers a new empty role. Returns ErrEmptyName if name is\n// empty, ErrRoleExists if a role of that name already exists.\nfunc (g *Group) AddRole(name string) (*Role, error) {\n\tif name == \"\" {\n\t\treturn nil, ErrEmptyName\n\t}\n\tif g.roles.Has(name) {\n\t\treturn nil, ErrRoleExists\n\t}\n\tr := newRole(name)\n\tg.roles.Set(name, r)\n\treturn r, nil\n}\n\n// GetRole returns the mutable role if it exists.\n//\n// SECURITY: the returned *Role exposes mutators (Members().Add/Remove,\n// SetMeta). Do not pass it to untrusted callers — use GetRole on a\n// *ReadonlyGroup for cross-realm exposure.\nfunc (g *Group) GetRole(name string) (r *Role, found bool) {\n\tr, found = g.roles.Get(name).(*Role)\n\treturn r, found\n}\n\n// HasRole reports whether a role with the given name exists.\nfunc (g *Group) HasRole(name string) bool {\n\treturn g.roles.Has(name)\n}\n\n// RemoveRole removes the named role and its membership records. Members of\n// the removed role are NOT removed from the base set or from any other\n// role; only this role's own data is discarded. Returns false if no such\n// role exists.\nfunc (g *Group) RemoveRole(name string) (removed bool) {\n\t_, removed = g.roles.Remove(name)\n\treturn removed\n}\n\n// RoleCount returns the number of registered roles.\nfunc (g *Group) RoleCount() int {\n\treturn g.roles.Size()\n}\n\n// IterateRoles walks roles in lexicographic name order, starting at offset\n// and visiting up to count roles. The callback receives a *ReadonlyRole —\n// deliberately not a *Role, so that plumbing an untrusted callback into the\n// iteration cannot escalate into role mutation under this realm's authority.\n// To mutate, capture names during iteration and revisit via GetRole from a\n// trusted context after iteration returns; registry mutation (AddRole,\n// RemoveRole) mid-iteration can panic and abort the transaction. fn returns\n// true to stop; IterateRoles returns true if stopped early.\nfunc (g *Group) IterateRoles(offset, count int, fn func(*ReadonlyRole) bool) (stopped bool) {\n\treturn g.roles.IterateByOffset(offset, count, func(_ string, value any) bool {\n\t\treturn fn(value.(*Role).Readonly())\n\t})\n}\n\n// --- Aggregations across base + all roles ---\n\n// HasAny reports whether addr is in the base set OR in any role.\nfunc (g *Group) HasAny(addr address) bool {\n\tif g.base.Has(addr) {\n\t\treturn true\n\t}\n\tfound := false\n\tg.roles.IterateByOffset(0, g.roles.Size(), func(_ string, value any) bool {\n\t\tif value.(*Role).members.Has(addr) {\n\t\t\tfound = true\n\t\t\treturn true // stop\n\t\t}\n\t\treturn false\n\t})\n\treturn found\n}\n\n// TotalSize returns the count of distinct addresses across the base set and\n// all roles, deduplicated. A caller may place the same address in base and\n// in multiple roles; TotalSize counts it once.\n//\n// Implementation note: dedup tracks seen addresses in an internal addrset,\n// costing O(N) memory in the total membership.\nfunc (g *Group) TotalSize() int {\n\tn := 0\n\tg.visitDistinct(func(address) bool {\n\t\tn++\n\t\treturn false\n\t})\n\treturn n\n}\n\n// IterateAll walks every distinct address across base + all roles,\n// deduplicated. Order: base first (in addrset order), then roles in name\n// order, skipping addresses already yielded. offset and count apply to the\n// deduplicated output, not the pre-dedup items; a negative offset counts as\n// zero. fn returns true to stop; IterateAll returns true if stopped early.\n//\n// The walk is live: do not mutate the group (base set, member sets, or the\n// role registry) from within fn — registry mutation mid-iteration can panic\n// and abort the transaction. Collect addresses first, mutate after\n// IterateAll returns.\n//\n// Implementation note: dedup tracks seen addresses in an internal addrset\n// (O(N) memory in the addresses scanned); scanning stops as soon as the\n// requested window has been served. For paginating a large Group without\n// dedup, use Iterate (base only) or IterateRoles.\nfunc (g *Group) IterateAll(offset, count int, fn func(addr address) bool) (stopped bool) {\n\tif count \u003c= 0 {\n\t\treturn false\n\t}\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\tseen := 0\n\tg.visitDistinct(func(a address) bool {\n\t\tif seen \u003c offset {\n\t\t\tseen++\n\t\t\treturn false\n\t\t}\n\t\tif fn(a) {\n\t\t\tstopped = true\n\t\t\treturn true\n\t\t}\n\t\tseen++\n\t\treturn seen-offset \u003e= count\n\t})\n\treturn stopped\n}\n\n// visitDistinct walks the base set then every role (in name order), calling\n// visit once per distinct address the first time it is seen. visit returns\n// true to stop the walk early.\nfunc (g *Group) visitDistinct(visit func(addr address) bool) {\n\tseen := \u0026addrset.Set{}\n\tdone := false\n\trecord := func(a address) bool {\n\t\tif !seen.Add(a) { // Add returns false when already seen\n\t\t\treturn false\n\t\t}\n\t\tdone = visit(a)\n\t\treturn done\n\t}\n\tg.base.IterateByOffset(0, g.base.Size(), record)\n\tif done {\n\t\treturn\n\t}\n\tg.roles.IterateByOffset(0, g.roles.Size(), func(_ string, value any) bool {\n\t\tr := value.(*Role)\n\t\tr.members.IterateByOffset(0, r.members.Size(), record)\n\t\treturn done\n\t})\n}\n\n// RolesContaining returns the names of all roles containing addr, in\n// lexicographic name order. The base set is not consulted (base membership\n// is not a \"role\"). Returns nil if addr is in no roles.\nfunc (g *Group) RolesContaining(addr address) []string {\n\tvar names []string\n\tg.roles.IterateByOffset(0, g.roles.Size(), func(name string, value any) bool {\n\t\tif value.(*Role).members.Has(addr) {\n\t\t\tnames = append(names, name)\n\t\t}\n\t\treturn false\n\t})\n\treturn names\n}\n\n// RemoveFromAll removes addr from the base set and from every role. Returns\n// true if it was removed from at least one location.\nfunc (g *Group) RemoveFromAll(addr address) (removed bool) {\n\tif g.base.Remove(addr) {\n\t\tremoved = true\n\t}\n\tg.roles.IterateByOffset(0, g.roles.Size(), func(_ string, value any) bool {\n\t\tif value.(*Role).members.Remove(addr) {\n\t\t\tremoved = true\n\t\t}\n\t\treturn false\n\t})\n\treturn removed\n}\n\n// Readonly returns a read-only view of the group.\nfunc (g *Group) Readonly() *ReadonlyGroup {\n\treturn \u0026ReadonlyGroup{group: g}\n}\n"},{"name":"readonly.gno","body":"package groups\n\nimport \"gno.land/p/moul/addrset\"\n\n// ReadonlyRole is a read-only view of a Role. It exposes only read-side\n// methods and holds the *Role in an unexported field, so cross-package\n// callers cannot mutate the role through this type.\ntype ReadonlyRole struct {\n\trole *Role\n}\n\n// Name returns the role's name.\nfunc (rr ReadonlyRole) Name() string {\n\treturn rr.role.name\n}\n\n// Members returns a read-only view of the role's member set.\nfunc (rr ReadonlyRole) Members() *addrset.ReadonlySet {\n\treturn rr.role.members.Readonly()\n}\n\n// Meta returns the role's metadata slot.\n//\n// NOTE: a mutable pointer stored in meta is NOT protected by this readonly\n// view — the pointee remains mutable by anyone who retrieves it. See the\n// package doc.\nfunc (rr ReadonlyRole) Meta() any {\n\treturn rr.role.meta\n}\n\n// ReadonlyGroup is a read-only view of a Group. Every method mirrors the\n// read-side of Group; mutators are absent. It holds the *Group in an\n// unexported field, so cross-package callers cannot mutate through it.\ntype ReadonlyGroup struct {\n\tgroup *Group\n}\n\n// --- Base set (base only) ---\n\n// Has reports whether addr is in the base set (roles not consulted).\nfunc (rg ReadonlyGroup) Has(addr address) bool {\n\treturn rg.group.Has(addr)\n}\n\n// Size returns the number of addresses in the base set only.\nfunc (rg ReadonlyGroup) Size() int {\n\treturn rg.group.Size()\n}\n\n// Iterate walks the base set (only); see Group.Iterate.\nfunc (rg ReadonlyGroup) Iterate(offset, count int, fn func(addr address) bool) (stopped bool) {\n\treturn rg.group.Iterate(offset, count, fn)\n}\n\n// --- Role registry ---\n\n// GetRole returns a read-only view of the named role if it exists.\nfunc (rg ReadonlyGroup) GetRole(name string) (rr *ReadonlyRole, found bool) {\n\tr, ok := rg.group.GetRole(name)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn r.Readonly(), true\n}\n\n// HasRole reports whether a role with the given name exists.\nfunc (rg ReadonlyGroup) HasRole(name string) bool {\n\treturn rg.group.HasRole(name)\n}\n\n// RoleCount returns the number of registered roles.\nfunc (rg ReadonlyGroup) RoleCount() int {\n\treturn rg.group.RoleCount()\n}\n\n// IterateRoles walks roles in name order; see Group.IterateRoles.\nfunc (rg ReadonlyGroup) IterateRoles(offset, count int, fn func(*ReadonlyRole) bool) (stopped bool) {\n\treturn rg.group.IterateRoles(offset, count, fn)\n}\n\n// --- Aggregations ---\n\n// HasAny reports whether addr is in the base set OR in any role.\nfunc (rg ReadonlyGroup) HasAny(addr address) bool {\n\treturn rg.group.HasAny(addr)\n}\n\n// TotalSize returns the deduplicated count across base + all roles.\nfunc (rg ReadonlyGroup) TotalSize() int {\n\treturn rg.group.TotalSize()\n}\n\n// IterateAll walks every distinct address across base + all roles; see\n// Group.IterateAll.\nfunc (rg ReadonlyGroup) IterateAll(offset, count int, fn func(addr address) bool) (stopped bool) {\n\treturn rg.group.IterateAll(offset, count, fn)\n}\n\n// RolesContaining returns the names of all roles containing addr, in name\n// order; see Group.RolesContaining.\nfunc (rg ReadonlyGroup) RolesContaining(addr address) []string {\n\treturn rg.group.RolesContaining(addr)\n}\n"},{"name":"role.gno","body":"package groups\n\nimport \"gno.land/p/moul/addrset\"\n\n// Role is a named bucket of addresses with optional metadata.\n//\n// A Role is always owned by a parent Group; the only way to obtain a *Role\n// is Group.AddRole or Group.GetRole. See the Group doc for the realm-\n// boundary rules that govern passing *Role values around.\ntype Role struct {\n\tname    string\n\tmembers *addrset.Set\n\tmeta    any\n}\n\n// newRole constructs a new empty role with the given name. Unexported: the\n// only valid path to a *Role is via Group.AddRole, which registers it in\n// the parent Group's role registry. A detached Role has no useful API.\nfunc newRole(name string) *Role {\n\treturn \u0026Role{\n\t\tname:    name,\n\t\tmembers: \u0026addrset.Set{},\n\t}\n}\n\n// Name returns the role's registry name.\nfunc (r *Role) Name() string {\n\treturn r.name\n}\n\n// Members returns a mutable reference to the role's member set; mutations\n// through the returned pointer affect the role.\n//\n// SECURITY: the returned *addrset.Set is mutable. Do not expose it to\n// untrusted callers — use Role.Readonly().Members() for a\n// cross-realm-safe view.\nfunc (r *Role) Members() *addrset.Set {\n\treturn r.members\n}\n\n// Meta returns the role's metadata slot. See the package doc for the rule\n// against storing mutable pointers in meta.\nfunc (r *Role) Meta() any {\n\treturn r.meta\n}\n\n// SetMeta sets the role's metadata slot. Passing nil clears it.\n//\n// SECURITY: do NOT store a pointer whose type has a mutator method (this\n// includes common /p/ types like *addrset.Set or *avl.Tree) if untrusted\n// realms may hold a Readonly() view of this Group. Meta() returns the stored\n// value as-is, so a foreign reader can invoke that method and borrow rule #2\n// commits the write under this (the allocating) realm's authority. A direct\n// field write through the pointer is still blocked by the realm-ownership\n// gate — the leak is specifically mutator-method dispatch. Prefer value types\n// with no internal pointers. See the package doc.\nfunc (r *Role) SetMeta(meta any) {\n\tr.meta = meta\n}\n\n// Readonly returns a read-only view of the role.\nfunc (r *Role) Readonly() *ReadonlyRole {\n\treturn \u0026ReadonlyRole{role: r}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"permissions","path":"gno.land/p/gnoland/boards/exts/permissions","files":[{"name":"README.md","body":"# Boards Permissions Extension\n\nThis is a `gno.land/p/gnoland/boards` package extension that provides a custom\n`Permissions` implementation that uses an underlying `gno.land/p/nt/groups`\ngroup to manage users and roles.\n\nIt also supports optionally setting validation functions to be triggered by the\n`WithPermission()` method before a callback is called. Validators allows adding\ncustom checks and requirements before the callback is called.\n\nUsage Example:\n\n[embedmd]:# (example_test.gno go)\n```go\npackage permissions\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Example user account\nconst user address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\n// Define a role\nconst RoleExample boards.Role = \"example\"\n\n// Define a permission\nconst PermissionFoo boards.Permission = 42\n\nfunc ExamplePermission() {\n\t// Define a custom foo permission validation function\n\tvalidateFoo := func(_ boards.Permissions, args boards.Args) error {\n\t\t// Check that the first argument is the string \"bob\"\n\t\tif name, ok := args[0].(string); !ok || name != \"bob\" {\n\t\t\treturn errors.New(\"unauthorized\")\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Create a permissions instance and assign the custom validator to it\n\tperms := New()\n\tperms.ValidateFunc(PermissionFoo, validateFoo)\n\n\t// Add foo permission to example role\n\tperms.AddRole(RoleExample, PermissionFoo)\n\n\t// Add a guest user\n\tperms.SetUserRoles(user, RoleExample)\n\n\t// Call a permissioned callback\n\targs := boards.Args{\"bob\"}\n\tperms.WithPermission(user, PermissionFoo, args, func() {\n\t\tprintln(\"Hello Bob!\")\n\t})\n\n\t// Output:\n\t// Hello Bob!\n}\n```\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/gnoland/boards/exts/permissions\"\ngno = \"0.9\"\n"},{"name":"options.gno","body":"package permissions\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Option configures permissions.\ntype Option func(*Permissions)\n\n// UseSingleUserRole configures permissions to only allow one role per user.\nfunc UseSingleUserRole() Option {\n\treturn func(p *Permissions) {\n\t\tp.singleUserRole = true\n\t}\n}\n\n// WithSuperRole configures permissions to have a super role.\n// A super role is the one that have all permissions.\n// This type of role doesn't need to be mapped to any permission.\nfunc WithSuperRole(r boards.Role) Option {\n\treturn func(p *Permissions) {\n\t\tif p.superRole != \"\" {\n\t\t\tpanic(\"permissions super role can be assigned only once\")\n\t\t}\n\n\t\tname := string(r)\n\t\tif strings.TrimSpace(name) == \"\" {\n\t\t\tpanic(\"permissions super role name is required\")\n\t\t}\n\n\t\tif _, err := p.group.AddRole(name); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tp.superRole = r\n\t}\n}\n"},{"name":"permissions.gno","body":"package permissions\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/groups/v0\"\n)\n\n// ValidatorFunc defines a function type for permissions validators.\n//\n// SECURITY: validators run inside WithPermission holding the live\n// Permissions value, with full mutation access (SetUserRoles, RemoveUser,\n// AddRole, ...) under the owning realm's authority. Register only functions\n// the owning realm controls, and never expose ValidateFunc or the\n// *Permissions value across a realm boundary.\ntype ValidatorFunc func(boards.Permissions, boards.Args) error\n\n// Permissions manages users, roles and permissions.\n//\n// This type is a default `gno.land/p/gnoland/boards` package `Permissions`\n// implementation that handles boards users, roles and permissions using an\n// underlying groups.Group: the base set holds every user (guests included),\n// and each boards role is a group Role whose member set is kept a subset of\n// the base set, with the role's boards.PermissionSet stored in the role meta\n// (a value type, per the groups meta rule). It also supports optionally\n// setting validation functions to be triggered within `WithPermission()`\n// method before a permissioned callback is called.\n//\n// No permissions validation is done by default.\n//\n// Users are allowed to have multiple roles at the same time by default, but\n// permissions can be configured to only allow one role per user.\ntype Permissions struct {\n\tsuperRole      boards.Role\n\tgroup          *groups.Group\n\tpublic         boards.PermissionSet\n\tvalidators     *bptree.BPTree // string(boards.Permission) -\u003e ValidatorFunc\n\tsingleUserRole bool\n}\n\n// New creates a new permissions type.\nfunc New(options ...Option) *Permissions {\n\tps := \u0026Permissions{\n\t\tvalidators: bptree.NewBPTree32(),\n\t\tgroup:      groups.NewGroup(),\n\t}\n\n\tfor _, apply := range options {\n\t\tapply(ps)\n\t}\n\treturn ps\n}\n\n// ValidateFunc adds a custom permission validator function.\n// If an existing permission function exists it's overwritten by the new one.\nfunc (ps *Permissions) ValidateFunc(p boards.Permission, fn ValidatorFunc) {\n\tps.validators.Set(p.String(), fn)\n}\n\n// SetPublicPermissions assigns permissions that are available to anyone.\n// It removes previous public permissions and assigns the new ones.\n// By default there are no public permissions.\nfunc (ps *Permissions) SetPublicPermissions(permissions ...boards.Permission) {\n\tps.public = boards.NewPermissionSet(permissions...)\n}\n\n// AddRole adds a role with one or more assigned permissions.\n// If role exists its permissions are overwritten with the new ones.\nfunc (ps *Permissions) AddRole(r boards.Role, p boards.Permission, extra ...boards.Permission) {\n\tname := string(r)\n\tif strings.TrimSpace(name) == \"\" {\n\t\tpanic(\"role name is required\")\n\t}\n\n\t// If role is the super role it already has all permissions\n\tif ps.superRole == r {\n\t\treturn\n\t}\n\n\t// Get the role if it exists or otherwise register a new one\n\trole, found := ps.group.GetRole(name)\n\tif !found {\n\t\tvar err error\n\t\trole, err = ps.group.AddRole(name)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// Save permissions within the role meta overwriting any existing permissions\n\tpermissions := append([]boards.Permission{p}, extra...)\n\trole.SetMeta(boards.NewPermissionSet(permissions...))\n}\n\n// RoleExists checks if a role exists.\nfunc (ps Permissions) RoleExists(r boards.Role) bool {\n\treturn r == ps.superRole || ps.group.HasRole(string(r))\n}\n\n// GetUserRoles returns the list of roles assigned to a user.\nfunc (ps Permissions) GetUserRoles(user address) []boards.Role {\n\tnames := ps.group.RolesContaining(user)\n\tif names == nil {\n\t\treturn nil\n\t}\n\n\troles := make([]boards.Role, len(names))\n\tfor i, name := range names {\n\t\troles[i] = boards.Role(name)\n\t}\n\treturn roles\n}\n\n// HasRole checks if a user has a specific role assigned.\nfunc (ps Permissions) HasRole(user address, r boards.Role) bool {\n\trole, found := ps.group.GetRole(string(r))\n\tif !found {\n\t\treturn false\n\t}\n\treturn role.Members().Has(user)\n}\n\n// HasPermission checks if a user has a specific permission.\nfunc (ps Permissions) HasPermission(user address, perm boards.Permission) bool {\n\tif ps.public.Has(perm) {\n\t\treturn true\n\t}\n\n\tfor _, name := range ps.group.RolesContaining(user) {\n\t\tif ps.superRole == boards.Role(name) {\n\t\t\treturn true\n\t\t}\n\n\t\trole, found := ps.group.GetRole(name)\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tif perms, ok := role.Meta().(boards.PermissionSet); ok \u0026\u0026 perms.Has(perm) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// SetUserRoles adds a new user when it doesn't exist and sets its roles.\n// Method can also be called to change the roles of an existing user.\n// It removes any existing user roles before assigning new ones.\n// All user's roles can be removed by calling this method without roles.\nfunc (ps *Permissions) SetUserRoles(user address, roles ...boards.Role) {\n\tif len(roles) \u003e 1 \u0026\u0026 ps.singleUserRole {\n\t\tpanic(\"user can only have one role\")\n\t}\n\n\t// Resolve every role name upfront so an invalid name panics before any\n\t// state is mutated.\n\tnewRoles := make([]*groups.Role, len(roles))\n\tfor i, r := range roles {\n\t\trole, found := ps.group.GetRole(string(r))\n\t\tif !found {\n\t\t\tpanic(\"invalid role: \" + string(r))\n\t\t}\n\t\tnewRoles[i] = role\n\t}\n\n\t// Clear current user roles\n\tfor _, name := range ps.group.RolesContaining(user) {\n\t\tif role, found := ps.group.GetRole(name); found {\n\t\t\trole.Members().Remove(user)\n\t\t}\n\t}\n\n\t// Every user is a base set member, with or without roles; role member\n\t// sets are kept subsets of the base set.\n\tps.group.Add(user)\n\n\t// Add user to role member sets\n\tfor _, role := range newRoles {\n\t\trole.Members().Add(user)\n\t}\n}\n\n// RemoveUser removes a user from permissions.\nfunc (ps *Permissions) RemoveUser(user address) bool {\n\treturn ps.group.RemoveFromAll(user)\n}\n\n// HasUser checks if a user exists.\nfunc (ps Permissions) HasUser(user address) bool {\n\treturn ps.group.Has(user)\n}\n\n// UsersCount returns the total number of users the permissioner contains.\nfunc (ps Permissions) UsersCount() int {\n\treturn ps.group.Size()\n}\n\n// IterateUsers iterates permissions' users.\nfunc (ps Permissions) IterateUsers(start, count int, fn boards.UsersIterFn) (stopped bool) {\n\treturn ps.group.Iterate(start, count, func(addr address) bool {\n\t\treturn fn(boards.User{\n\t\t\tAddress: addr,\n\t\t\tRoles:   ps.GetUserRoles(addr),\n\t\t})\n\t})\n}\n\n// WithPermission calls a callback when a user has a specific permission.\n// It panics on error or when a permission validator fails.\n// Callbacks are by default called when there is no validator function registered for the permission.\n// If a permission validation function exists it's called before calling the callback.\nfunc (ps *Permissions) WithPermission(user address, p boards.Permission, args boards.Args, cb func()) {\n\tif !ps.HasPermission(user, p) {\n\t\tpanic(\"unauthorized, user \" + user.String() + \" doesn't have the required permission\")\n\t}\n\n\t// Execute custom validation before calling the callback\n\tif v := ps.validators.Get(p.String()); v != nil {\n\t\terr := v.(ValidatorFunc)(ps, args)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tcb()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"pager","path":"gno.land/p/jeronimoalbi/pager","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/jeronimoalbi/pager\"\ngno = \"0.9\"\n"},{"name":"pager.gno","body":"// Package pager provides pagination functionality through a generic pager implementation.\n//\n// Example usage:\n//\n//\timport (\n//\t    \"strconv\"\n//\t    \"strings\"\n//\n//\t    \"gno.land/p/jeronimoalbi/pager\"\n//\t)\n//\n//\tfunc Render(path string) string {\n//\t    // Define the items to paginate\n//\t    items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n//\n//\t    // Create a pager that paginates 4 items at a time\n//\t    p, err := pager.New(path, len(items), pager.WithPageSize(4))\n//\t    if err != nil {\n//\t        panic(err)\n//\t    }\n//\n//\t    // Render items for the current page\n//\t    var output strings.Builder\n//\t    p.Iterate(func(i int) bool {\n//\t        output.WriteString(\"- \" + strconv.Itoa(items[i]) + \"\\n\")\n//\t        return false\n//\t    })\n//\n//\t    // Render page picker\n//\t    if p.HasPages() {\n//\t        output.WriteString(\"\\n\" + pager.Picker(p))\n//\t    }\n//\n//\t    return output.String()\n//\t}\npackage pager\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar ErrInvalidPageNumber = errors.New(\"invalid page number\")\n\n// PagerIterFn defines a callback to iterate page items.\ntype PagerIterFn func(index int) (stop bool)\n\n// New creates a new pager.\nfunc New(rawURL string, totalItems int, options ...PagerOption) (Pager, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn Pager{}, err\n\t}\n\n\tp := Pager{\n\t\tquery:          u.RawQuery,\n\t\tpageQueryParam: DefaultPageQueryParam,\n\t\tpageSize:       DefaultPageSize,\n\t\tpage:           1,\n\t\ttotalItems:     totalItems,\n\t}\n\tfor _, apply := range options {\n\t\tapply(\u0026p)\n\t}\n\n\tp.pageCount = int(math.Ceil(float64(p.totalItems) / float64(p.pageSize)))\n\n\trawPage := u.Query().Get(p.pageQueryParam)\n\tif rawPage != \"\" {\n\t\tp.page, _ = strconv.Atoi(rawPage)\n\t\tif p.page == 0 || p.page \u003e p.pageCount {\n\t\t\treturn Pager{}, ErrInvalidPageNumber\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\n// MustNew creates a new pager or panics if there is an error.\nfunc MustNew(rawURL string, totalItems int, options ...PagerOption) Pager {\n\tp, err := New(rawURL, totalItems, options...)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn p\n}\n\n// Pager allows paging items.\ntype Pager struct {\n\tquery, pageQueryParam                 string\n\tpageSize, page, pageCount, totalItems int\n}\n\n// TotalItems returns the total number of items to paginate.\nfunc (p Pager) TotalItems() int {\n\treturn p.totalItems\n}\n\n// PageSize returns the size of each page.\nfunc (p Pager) PageSize() int {\n\treturn p.pageSize\n}\n\n// Page returns the current page number.\nfunc (p Pager) Page() int {\n\treturn p.page\n}\n\n// PageCount returns the number pages.\nfunc (p Pager) PageCount() int {\n\treturn p.pageCount\n}\n\n// Offset returns the index of the first page item.\nfunc (p Pager) Offset() int {\n\treturn (p.page - 1) * p.pageSize\n}\n\n// HasPages checks if pager has more than one page.\nfunc (p Pager) HasPages() bool {\n\treturn p.pageCount \u003e 1\n}\n\n// GetPageURI returns the URI for a page.\n// An empty string is returned when page doesn't exist.\nfunc (p Pager) GetPageURI(page int) string {\n\tif page \u003c 1 || page \u003e p.PageCount() {\n\t\treturn \"\"\n\t}\n\n\tvalues, _ := url.ParseQuery(p.query)\n\tvalues.Set(p.pageQueryParam, strconv.Itoa(page))\n\treturn \"?\" + values.Encode()\n}\n\n// PrevPageURI returns the URI path to the previous page.\n// An empty string is returned when current page is the first page.\nfunc (p Pager) PrevPageURI() string {\n\tif p.page == 1 || !p.HasPages() {\n\t\treturn \"\"\n\t}\n\treturn p.GetPageURI(p.page - 1)\n}\n\n// NextPageURI returns the URI path to the next page.\n// An empty string is returned when current page is the last page.\nfunc (p Pager) NextPageURI() string {\n\tif p.page == p.pageCount {\n\t\t// Current page is the last page\n\t\treturn \"\"\n\t}\n\treturn p.GetPageURI(p.page + 1)\n}\n\n// Iterate allows iterating page items.\nfunc (p Pager) Iterate(fn PagerIterFn) bool {\n\tif p.totalItems == 0 {\n\t\treturn true\n\t}\n\n\tstart := p.Offset()\n\tend := start + p.PageSize()\n\tif end \u003e p.totalItems {\n\t\tend = p.totalItems\n\t}\n\n\tfor i := start; i \u003c end; i++ {\n\t\tif fn(i) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// TODO: Support different types of pickers (ex. with clickable page numbers)\n\n// Picker returns a string with the pager as Markdown.\n// An empty string is returned when the pager has no pages.\nfunc Picker(p Pager) string {\n\tif !p.HasPages() {\n\t\treturn \"\"\n\t}\n\n\tvar out strings.Builder\n\n\tif s := p.PrevPageURI(); s != \"\" {\n\t\tout.WriteString(\"[«](\" + s + \") | \")\n\t} else {\n\t\tout.WriteString(\"\\\\- | \")\n\t}\n\n\tout.WriteString(\"page \" + strconv.Itoa(p.Page()) + \" of \" + strconv.Itoa(p.PageCount()))\n\n\tif s := p.NextPageURI(); s != \"\" {\n\t\tout.WriteString(\" | [»](\" + s + \")\")\n\t} else {\n\t\tout.WriteString(\" | \\\\-\")\n\t}\n\n\treturn out.String()\n}\n"},{"name":"pager_options.gno","body":"package pager\n\nimport \"strings\"\n\nconst (\n\tDefaultPageSize       = 50\n\tDefaultPageQueryParam = \"page\"\n)\n\n// PagerOption configures the pager.\ntype PagerOption func(*Pager)\n\n// WithPageSize assigns a page size to a pager.\nfunc WithPageSize(size int) PagerOption {\n\treturn func(p *Pager) {\n\t\tif size \u003c 1 {\n\t\t\tp.pageSize = DefaultPageSize\n\t\t} else {\n\t\t\tp.pageSize = size\n\t\t}\n\t}\n}\n\n// WithPageQueryParam assigns the name of the URL query param for the page value.\nfunc WithPageQueryParam(name string) PagerOption {\n\treturn func(p *Pager) {\n\t\tname = strings.TrimSpace(name)\n\t\tif name == \"\" {\n\t\t\tname = DefaultPageQueryParam\n\t\t}\n\t\tp.pageQueryParam = name\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"svgbtn","path":"gno.land/p/leon/svgbtn","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/leon/svgbtn\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"},{"name":"svgbtn.gno","body":"// Package svgbtn provides utilities for generating SVG-styled buttons as Markdown image links.\n//\n// Buttons are rendered as SVG images with customizable size, colors, labels, and links.\n// This package includes preconfigured styles such as Primary, Danger, Success, Small, Wide,\n// Text-like, and Icon buttons, as well as a factory method for dynamic button creation.\n//\n// Example usage:\n//\n//\tfunc Render(_ string) string {\n//\t\tbtn := svgbtn.PrimaryButton(120, 40, \"Click Me\", \"https://example.com\")\n//\t\treturn btn\n//\t}\n//\n// See more examples at gno.land/r/leon:buttons\n//\n// All buttons are returned as Markdown-compatible strings: [svg_data](link).\npackage svgbtn\n\nimport (\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Button creates a base SVG button with given size, colors, label, and link.\n// - `width`, `height`: size in pixels\n// - `btnColor`: background color (e.g. \"#007BFF\")\n// - `textColor`: label color (e.g. \"#FFFFFF\")\n// - `text`: visible button label\n// - `link`: URL to wrap the image in markdown-style [svg](link)\nfunc Button(width, height int, btnColor, textColor, text, link string) string {\n\treturn ButtonWithRadius(width, height, height/5, btnColor, textColor, text, link)\n}\n\n// ButtonWithRadius creates a base SVG button with custom border radius.\n// - `width`, `height`: size in pixels\n// - `radius`: border radius in pixels\n// - `btnColor`: background color (e.g. \"#007BFF\")\n// - `textColor`: label color (e.g. \"#FFFFFF\")\n// - `text`: visible button label\n// - `link`: URL to wrap the image in markdown-style [svg](link)\nfunc ButtonWithRadius(width, height, radius int, btnColor, textColor, text, link string) string {\n\tcanvas := svg.NewCanvas(width, height).\n\t\tWithViewBox(0, 0, width, height).\n\t\tAddStyle(\"text\", \"font-family:sans-serif;font-size:14px;text-anchor:middle;dominant-baseline:middle;\")\n\n\tbg := svg.NewRectangle(0, 0, width, height, btnColor)\n\tbg.RX = radius\n\tbg.RY = radius\n\n\tlabel := svg.NewText(width/2, height/2, text, textColor)\n\n\tcanvas.Append(bg, label)\n\n\treturn ufmt.Sprintf(\"[%s](%s)\", canvas.Render(text), link)\n}\n\n// PrimaryButton renders a blue button with white text.\nfunc PrimaryButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#007BFF\", \"#ffffff\", text, link)\n}\n\n// DangerButton renders a red button with white text.\nfunc DangerButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#DC3545\", \"#ffffff\", text, link)\n}\n\n// SuccessButton renders a green button with white text.\nfunc SuccessButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#28A745\", \"#ffffff\", text, link)\n}\n\n// SmallButton renders a compact gray button with white text.\nfunc SmallButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#6C757D\", \"#ffffff\", text, link)\n}\n\n// WideButton renders a wider cyan button with white text.\nfunc WideButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#17A2B8\", \"#ffffff\", text, link)\n}\n\n// TextButton renders a white button with colored text, like a hyperlink.\nfunc TextButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#ffffff\", \"#007BFF\", text, link)\n}\n\n// IconButton renders a square button with an icon character (e.g. emoji).\nfunc IconButton(width, height int, icon, link string) string {\n\treturn Button(width, height, \"#E0E0E0\", \"#000000\", icon, link)\n}\n\n// ButtonFactory provides a named-style constructor for buttons.\n// Supported kinds: \"primary\", \"danger\", \"success\", \"small\", \"wide\", \"text\", \"icon\".\nfunc ButtonFactory(kind string, width, height int, text, link string) string {\n\tswitch kind {\n\tcase \"primary\":\n\t\treturn PrimaryButton(width, height, text, link)\n\tcase \"danger\":\n\t\treturn DangerButton(width, height, text, link)\n\tcase \"success\":\n\t\treturn SuccessButton(width, height, text, link)\n\tcase \"small\":\n\t\treturn SmallButton(width, height, text, link)\n\tcase \"wide\":\n\t\treturn WideButton(width, height, text, link)\n\tcase \"text\":\n\t\treturn TextButton(width, height, text, link)\n\tcase \"icon\":\n\t\treturn IconButton(width, height, text, link)\n\tdefault:\n\t\treturn PrimaryButton(width, height, text, link)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"foreign","path":"gno.land/p/nt/markdown/foreign/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `foreign` - Foreign markdown sandbox\n\nRealm-side helper that wraps externally-built markdown in a `\u003cgno-foreign\u003e` sandbox block. gnoweb renders the wrapped body inside its own goldmark sub-instance, so markdown you did not author cannot reach out and alter the surrounding page. Use it when flowing in markdown returned by another realm's interface method, fetched from chain storage owned by another realm, or otherwise outside your control.\n\n## Usage\n\n```go\npackage myrealm\n\nimport \"gno.land/p/nt/markdown/foreign/v0\"\n\nfunc Render(path string) string {\n    body := otherRealm.Render(path) // markdown you did not author\n    return \"## Included content\\n\\n\" + foreign.Foreign(body)\n}\n```\n\nWith a caller-supplied label shown as a strip above the body:\n\n```go\nforeign.ForeignWithLabel(\"Pulled from /r/foo\", body)\n```\n\n## API\n\n```go\nfunc Foreign(body string) string\nfunc ForeignWithLabel(label, body string) string\nfunc MaxBlocksPerRender() int\n```\n\n## Notes\n\n- `body` is normalized before wrapping: `\\r\\n` and bare `\\r` become `\\n`, and any line that looks like a `gno-foreign` opener or closer (bare, attribute-bearing, or any case) has its leading `\u003c` escaped to `\u0026lt;`. Foreign content therefore cannot terminate the sandbox early or open a nested one.\n- `ForeignWithLabel` sanitizes the label: bidi/zero-width characters are stripped, NUL is dropped, other control characters and the Unicode line separators (U+2028/U+2029/U+0085) become spaces, `\u0026` `\u003c` `\u003e` `\"` become HTML entities, and surrounding whitespace is trimmed. A label that is empty after sanitization behaves exactly like `Foreign` (no label strip, no default text).\n- `MaxBlocksPerRender()` re-exports gnoweb's per-render cap on `\u003cgno-foreign\u003e` blocks (the same value the renderer reads). Past the cap, later blocks fall through to raw HTML and are dropped, so keep a page's foreign total under it.\n- The renderer-side contract lives in `gno.land/pkg/gnoweb/markdown/ext_foreign.go`.\n- To clean user-supplied (rather than realm-supplied) markdown at the leaf level, see [`sanitize`](../../sanitize/v0).\n"},{"name":"foreign.gno","body":"// Package foreign provides the realm-side helper for emitting the\n// gno-foreign sandbox block. Realm authors wrap externally-built\n// markdown (markdown returned by an interface method on a foreign\n// realm, fetched from chain storage owned by another realm, etc.) in\n// Foreign before flowing it into rendered output, so gnoweb renders\n// the body inside its own goldmark sub-instance with structural\n// extensions selectively loaded.\n//\n// The renderer-side contract lives in\n// gno.land/pkg/gnoweb/markdown/ext_foreign.go. This helper produces\n// bytes that satisfy the parser's opener requirements (CommonMark\n// §4.6 Type-7 HTML block, no attribute fall-through) and neutralizes\n// any literal sentinel lines in the body so the foreign markdown\n// cannot terminate the outer block prematurely.\npackage foreign\n\nimport (\n\t\"chain/markdown\"\n\t\"strings\"\n)\n\n// Foreign wraps body in a `\u003cgno-foreign\u003e` ... `\u003c/gno-foreign\u003e` sandbox\n// block. The returned string is ready to concatenate into a larger\n// markdown document.\n//\n// Three normalization steps apply to body:\n//\n//  1. \\r\\n and bare \\r line endings are normalized to \\n. The parser\n//     uses byte-equal matching against the sentinel close tag, so\n//     mixed line endings would otherwise change the match boundary.\n//\n//  2. Any line whose trimmed content looks like a gno-foreign tag\n//     opener OR closer — bare (`\u003cgno-foreign\u003e`, `\u003c/gno-foreign\u003e`) or\n//     attribute-bearing (`\u003cgno-foreign label=\"x\"\u003e`, `\u003c/gno-foreign\n//     attr=\"…\"\u003e`, etc.) — is neutralized by HTML-escaping the leading\n//     `\u003c` to `\u0026lt;`. The parser tokenizes line bytes literally, so the\n//     escaped form is seen as text and cannot terminate the outer\n//     block or open an unintended inner block.\n//\n//     Crucially, BOTH open-tag and close-tag attribute-bearing forms\n//     are neutralized. The parser recognizes a bare `\u003cgno-foreign\u003e`\n//     opener, a labeled `\u003cgno-foreign label=\"x\"\u003e` opener, and ANY\n//     `\u003c/gno-foreign…\u003e` closer (golang.org/x/net/html drops attrs on\n//     end tags before our recognizer sees them, so attr-bearing\n//     closers are sentinel-equivalent). Leaving any of those forms\n//     un-neutralized in body bytes would let attacker-supplied\n//     markdown adjust the parser's framing-depth counter and either\n//     consume the helper's own close (capturing trailing realm\n//     content into the sandbox) or close the outer block early\n//     (escaping the sandbox entirely).\n//\n//     There is therefore NO nesting via the helper: Foreign(Foreign(x))\n//     escapes the inner call's own `\u003cgno-foreign\u003e`/`\u003c/gno-foreign\u003e`\n//     lines, so the inner block renders as visible literal text inside\n//     one sandbox, not as a nested sandbox. This is intended — wrapping\n//     foreign-built markdown that itself contains gno-foreign sentinels\n//     must neutralize them, not honor them.\n//\n//  3. A leading and trailing blank line are emitted around the\n//     opener / closer. CommonMark §4.6 forbids Type-7 HTML blocks\n//     from interrupting a paragraph; without the blank line, an\n//     opener following a non-blank line is absorbed into the\n//     preceding paragraph instead of opening a sandbox.\n//\n// The renderer caps cross-family nesting at 4 levels and per-Convert\n// foreign blocks at 256. Beyond those caps, the opener falls through\n// to raw HTML and is stripped by the renderer's safe mode.\nfunc Foreign(body string) string {\n\treturn wrapForeign(\"\", body)\n}\n\n// ForeignWithLabel wraps body like Foreign but emits an explicit\n// `label=\"…\"` attribute on the opener so the rendered sandbox carries\n// a caller-supplied label (e.g., \"Pulled from /r/foo\") shown as a\n// strip above the body. The label is sanitized so it cannot inject\n// HTML or break out of the attribute value:\n//\n//   - NUL bytes are dropped.\n//   - Other control characters (U+0000–U+001F, U+007F) become spaces.\n//   - `\u0026`, `\u003c`, `\u003e`, and `\"` are replaced with their HTML entities.\n//   - Leading/trailing whitespace is trimmed.\n//\n// A label that is empty after sanitization behaves identically to\n// Foreign: no attribute is emitted, and the renderer shows the sandbox\n// box with NO label strip (there is no default label text).\nfunc ForeignWithLabel(label, body string) string {\n\treturn wrapForeign(label, body)\n}\n\n// MaxBlocksPerRender is gnoweb's per-render cap on the number of\n// \u003cgno-foreign\u003e blocks a single page render admits; beyond it, later\n// blocks fall through to raw HTML and are dropped. A realm emitting\n// many foreign blocks (e.g. one per comment) should keep its rendered\n// total under this. Re-exports chain/markdown.MaxForeignBlocksPerConvert\n// — the single source of truth the gnoweb renderer also reads — so\n// callers get the cap without importing chain/markdown directly.\nfunc MaxBlocksPerRender() int {\n\treturn markdown.MaxForeignBlocksPerConvert()\n}\n\nfunc wrapForeign(rawLabel, body string) string {\n\tlabel := sanitizeLabel(rawLabel)\n\n\t// Normalize line endings (CR/CRLF → LF). The parser matches the\n\t// sentinel close against \\n-delimited lines, so mixed line endings\n\t// would otherwise shift the match boundary. CR/CRLF → LF ONLY: do\n\t// not fold Unicode separators here — they must stay verbatim in the\n\t// body so the inner renderer sees the foreign markdown unaltered.\n\tbody = markdown.NormalizeBreaks(body)\n\n\t// Mangle any line that would terminate the outer block or open\n\t// an inner one. Covers bare and attribute-bearing forms of both\n\t// the opener and the closer (see step 2 in the package doc).\n\tvar b strings.Builder\n\t// b accumulates only the body lines (the opener/closer envelope is\n\t// concatenated separately below), so len(body) is the exact size in\n\t// the common case. Sentinel lines that expand `\u003c`→`\u0026lt;` may force\n\t// one growth — rare enough not to pre-size for.\n\tb.Grow(len(body))\n\tlines := strings.Split(body, \"\\n\")\n\tfor i, line := range lines {\n\t\tif isForeignSentinelLine(trimSentinel(line)) {\n\t\t\t// Escape just the leading `\u003c` so the html tokenizer\n\t\t\t// sees this as text instead of a tag. Preserve any 0-3\n\t\t\t// leading spaces the parser's trim would have stripped.\n\t\t\tidx := strings.Index(line, \"\u003c\")\n\t\t\tif idx \u003e= 0 {\n\t\t\t\tline = line[:idx] + \"\u0026lt;\" + line[idx+1:]\n\t\t\t}\n\t\t}\n\t\tb.WriteString(line)\n\t\tif i \u003c len(lines)-1 {\n\t\t\tb.WriteByte('\\n')\n\t\t}\n\t}\n\n\topener := \"\u003cgno-foreign\u003e\"\n\tif label != \"\" {\n\t\topener = `\u003cgno-foreign label=\"` + label + `\"\u003e`\n\t}\n\treturn \"\\n\\n\" + opener + \"\\n\" + b.String() + \"\\n\u003c/gno-foreign\u003e\\n\\n\"\n}\n\n// sanitizeLabel makes a user-supplied label safe to splice into an\n// HTML attribute value on the gno-foreign opener line.\nfunc sanitizeLabel(s string) string {\n\t// Strip bidi-override and zero-width controls FIRST — same ordering\n\t// as the sanitize package's HTMLEscape — so invisible reordering or\n\t// zero-width payloads can't survive into the rendered label.\n\ts = markdown.StripBidiAndZeroWidth(s)\n\t// Drop NUL; map other ASCII controls AND the Unicode line/paragraph\n\t// separators (U+2028, U+2029, U+0085 NEL) to spaces. The opener is a\n\t// single line, so any of these surviving in the label would either\n\t// add a control payload or, for the separators, render as a stray\n\t// line break inside the attribute.\n\ts = strings.Map(func(r rune) rune {\n\t\tif r == 0 {\n\t\t\treturn -1\n\t\t}\n\t\tif r \u003c 0x20 || r == 0x7f || r == 0x2028 || r == 0x2029 || r == 0x0085 {\n\t\t\treturn ' '\n\t\t}\n\t\treturn r\n\t}, s)\n\t// Escape `\u0026` first so subsequent entity bytes don't get\n\t// re-escaped.\n\ts = strings.ReplaceAll(s, \"\u0026\", \"\u0026amp;\")\n\ts = strings.ReplaceAll(s, `\"`, \"\u0026quot;\")\n\ts = strings.ReplaceAll(s, \"\u003c\", \"\u0026lt;\")\n\ts = strings.ReplaceAll(s, \"\u003e\", \"\u0026gt;\")\n\treturn strings.TrimSpace(s)\n}\n\n// isForeignSentinelLine reports whether s (already trimmed via\n// trimSentinel) begins with the gno-foreign tag prefix and so must be\n// neutralized before it can reach the renderer-side parser.\n//\n// Deliberately OVER-INCLUSIVE: it matches any line whose trimmed form\n// starts (case-INSENSITIVELY) with `\u003cgno-foreign` or `\u003c/gno-foreign`,\n// regardless of what follows. This is a strict superset of every line\n// goldmark's html.Tokenizer can recognize as a \u003cgno-foreign\u003e opener or\n// closer, which is what makes it safe:\n//\n//   - The tokenizer lowercases tag names, so `\u003cGNO-FOREIGN\u003e` etc. are\n//     sentinels; the prefix match is case-folded to mirror that.\n//   - The tokenizer ends a tag name at ANY of several terminators\n//     (`\u003e`, space, tab, form-feed, `/`). A precise check that\n//     enumerates terminators keeps missing variants — e.g.\n//     `\u003c/gno-foreign/\u003e` and `\u003c/gno-foreign\\f\u003e` are both recognized as\n//     closers by the parser. Matching on the prefix alone cannot miss\n//     one: if a body line could be parsed as a sentinel, it starts with\n//     this prefix and is escaped here.\n//\n// The only cost is that an unrelated longer tag like `\u003cgno-foreignx\u003e`\n// (a different tag name, not a sentinel) is also escaped — rendered as\n// visible literal text instead of being raw-HTML-stripped — which is\n// harmless for foreign body bytes.\nfunc isForeignSentinelLine(s string) bool {\n\treturn hasASCIIFoldPrefix(s, \"\u003c/gno-foreign\") || hasASCIIFoldPrefix(s, \"\u003cgno-foreign\")\n}\n\n// hasASCIIFoldPrefix reports whether s begins with prefix, comparing\n// ASCII letters case-insensitively. prefix must be lowercase ASCII;\n// folding is ASCII-only on purpose (Unicode case folding would\n// over-match, and the sentinel envelope is pure ASCII anyway).\nfunc hasASCIIFoldPrefix(s, prefix string) bool {\n\tif len(s) \u003c len(prefix) {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(prefix); i++ {\n\t\tc := s[i]\n\t\tif c \u003e= 'A' \u0026\u0026 c \u003c= 'Z' {\n\t\t\tc += 'a' - 'A'\n\t\t}\n\t\tif c != prefix[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// trimSentinel returns line with the leading 0-3 spaces and trailing\n// ASCII whitespace that the parser's trimForeignLine strips. Mirrors\n// the byte-level trim the parser performs so this helper detects the\n// same sentinel match the parser would.\nfunc trimSentinel(s string) string {\n\ti := 0\n\tfor i \u003c len(s) \u0026\u0026 i \u003c 3 \u0026\u0026 s[i] == ' ' {\n\t\ti++\n\t}\n\ts = s[i:]\n\tfor len(s) \u003e 0 {\n\t\tc := s[len(s)-1]\n\t\tif c == ' ' || c == '\\t' || c == '\\n' || c == '\\v' || c == '\\f' || c == '\\r' {\n\t\t\ts = s[:len(s)-1]\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\treturn s\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/markdown/foreign/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"mdalert","path":"gno.land/p/nt/mdalert/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `mdalert` - Markdown alerts\n\nRender gnoweb-flavored Markdown alert blocks (note, tip, info, success, warning, caution) with optional title and folded mode.\n\n## Usage\n\n```go\nimport \"gno.land/p/nt/mdalert/v0\"\n\n// One-liner helpers per type\nmd := mdalert.Warning(\"Heads up\", \"Disk almost full\")\n\n// Formatted variants accept ufmt-style args\nmd = mdalert.Infof(\"Stats\", \"%d users online\", n)\n\n// Full control via the Alert struct (e.g. folded by default)\na := mdalert.New(mdalert.TypeTip, \"Click to expand\", \"Hidden details here\", true)\nmd = a.String()\n```\n\nRendered output (for the warning above):\n\n```\n\u003e [!WARNING] Heads up\n\u003e Disk almost full\n```\n\n## API\n\n```go\n// Type identifies an alert variant.\ntype Type string\n\nconst (\n    TypeCaution Type = \"CAUTION\"\n    TypeInfo         = \"INFO\"\n    TypeNote         = \"NOTE\"\n    TypeSuccess      = \"SUCCESS\"\n    TypeTip          = \"TIP\"\n    TypeWarning      = \"WARNING\"\n)\n\n// Alert is a Markdown alert block.\ntype Alert struct {\n    Type    Type   // Alert variant\n    Title   string // Optional title (header line)\n    Message string // Body; may contain newlines\n    Folded  bool   // If true, render collapsed (only title visible)\n}\n\n// String renders the alert as Markdown. Returns \"\" if Type is empty or Message is blank (whitespace only).\nfunc (a Alert) String() string\n\n// New builds an Alert.\nfunc New(t Type, title, msg string, folded bool) Alert\n\n// Per-type helpers (unfolded). The *f variants format msg with ufmt.Sprintf.\nfunc Caution(title, msg string) string\nfunc Cautionf(title, format string, a ...any) string\nfunc Info(title, msg string) string\nfunc Infof(title, format string, a ...any) string\nfunc Note(title, msg string) string\nfunc Notef(title, format string, a ...any) string\nfunc Success(title, msg string) string\nfunc Successf(title, format string, a ...any) string\nfunc Tip(title, msg string) string\nfunc Tipf(title, format string, a ...any) string\nfunc Warning(title, msg string) string\nfunc Warningf(title, format string, a ...any) string\n```\n\n## Notes\n\n- Alert types are documented in the Markdown docs realm: [/r/docs/markdown#alerts](/r/docs/markdown#alerts).\n- Per-type helpers always render unfolded. For folded alerts use `New(...)` with `folded=true`.\n- `title` and `msg` are emitted into Markdown as-is. When either carries untrusted input (a `Render(path)` segment, user text), wrap it with `sanitize.InlineText` from [`gno.land/p/nt/markdown/sanitize/v0`](../../markdown/sanitize/v0) first, or it can inject structure into the rendered page. The `*f` variants format via `ufmt.Sprintf`, which supports only ufmt's verb subset (no `%b`, `%o`, `%w`, `%+v`).\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package mdalert provides support for creating Markdown alerts.\npackage mdalert\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/mdalert/v0\"\ngno = \"0.9\"\n"},{"name":"mdalert.gno","body":"// Package mdalert provides support for creating Markdown alerts.\n//\n// It defines supported alert types and helper functions that can be\n// called to generate Markdown for different alert types.\n//\n// The different alert types are documented in the Markdown docs realm:\n// https://gno.land/r/docs/markdown#alerts\npackage mdalert\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Types of alerts.\nconst (\n\tTypeCaution Type = \"CAUTION\"\n\tTypeInfo         = \"INFO\"\n\tTypeNote         = \"NOTE\"\n\tTypeSuccess      = \"SUCCESS\"\n\tTypeTip          = \"TIP\"\n\tTypeWarning      = \"WARNING\"\n)\n\ntype (\n\t// Type defines a type for the alert types.\n\tType string\n\n\t// Alert defines a type for alerts.\n\tAlert struct {\n\t\t// Type defines the type of alert.\n\t\tType Type\n\n\t\t// Title contains an optional title for the alert.\n\t\tTitle string\n\n\t\t// Message contains alerts's message.\n\t\tMessage string\n\n\t\t// Folded indicates that the alert must be folded on render.\n\t\t// Message is not initially visible when folded, only title is visible.\n\t\tFolded bool\n\t}\n)\n\n// String returns the alert as a Markdown string.\nfunc (a Alert) String() string {\n\talertType := string(a.Type)\n\tmsg := strings.TrimSpace(a.Message)\n\tif msg == \"\" || alertType == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// Init alert fold marker\n\tvar fold string\n\tif a.Folded {\n\t\tfold = \"-\"\n\t}\n\n\t// Write alert header\n\tvar b strings.Builder\n\theader := ufmt.Sprintf(\"\u003e [!%s]%s %s\", alertType, fold, a.Title)\n\tb.WriteString(strings.TrimSpace(header) + \"\\n\")\n\n\t// Write alert message\n\tlines := strings.Split(msg, \"\\n\")\n\tfor _, line := range lines {\n\t\tb.WriteString(\"\u003e \" + line + \"\\n\")\n\t}\n\treturn b.String()\n}\n\n// New creates a new alert.\nfunc New(t Type, title, msg string, folded bool) Alert {\n\treturn Alert{\n\t\tType:    t,\n\t\tTitle:   title,\n\t\tMessage: msg,\n\t\tFolded:  folded,\n\t}\n}\n\n// Caution returns an alert Markdown of type caution.\nfunc Caution(title, msg string) string {\n\treturn New(TypeCaution, title, msg, false).String()\n}\n\n// Cautionf returns an alert Markdown of type caution with a formatted message.\nfunc Cautionf(title, format string, a ...any) string {\n\treturn New(TypeCaution, title, ufmt.Sprintf(format, a...), false).String()\n}\n\n// Info returns an alert Markdown of type info.\nfunc Info(title, msg string) string {\n\treturn New(TypeInfo, title, msg, false).String()\n}\n\n// Infof returns an alert Markdown of type info with a formatted message.\nfunc Infof(title, format string, a ...any) string {\n\treturn New(TypeInfo, title, ufmt.Sprintf(format, a...), false).String()\n}\n\n// Note returns an alert Markdown of type note.\nfunc Note(title, msg string) string {\n\treturn New(TypeNote, title, msg, false).String()\n}\n\n// Notef returns an alert Markdown of type note with a formatted message.\nfunc Notef(title, format string, a ...any) string {\n\treturn New(TypeNote, title, ufmt.Sprintf(format, a...), false).String()\n}\n\n// Success returns an alert Markdown of type success.\nfunc Success(title, msg string) string {\n\treturn New(TypeSuccess, title, msg, false).String()\n}\n\n// Notef returns an alert Markdown of type success with a formatted message.\nfunc Successf(title, format string, a ...any) string {\n\treturn New(TypeSuccess, title, ufmt.Sprintf(format, a...), false).String()\n}\n\n// Tip returns an alert Markdown of type tip.\nfunc Tip(title, msg string) string {\n\treturn New(TypeTip, title, msg, false).String()\n}\n\n// Tipf returns an alert Markdown of type tip with a formatted message.\nfunc Tipf(title, format string, a ...any) string {\n\treturn New(TypeTip, title, ufmt.Sprintf(format, a...), false).String()\n}\n\n// Warning returns an alert Markdown of type warning.\nfunc Warning(title, msg string) string {\n\treturn New(TypeWarning, title, msg, false).String()\n}\n\n// Warningf returns an alert Markdown of type warning with a formatted message.\nfunc Warningf(title, format string, a ...any) string {\n\treturn New(TypeWarning, title, ufmt.Sprintf(format, a...), false).String()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"boards2","path":"gno.land/r/gnoland/boards2/v1","files":[{"name":"README.md","body":"# Boards2\n\nBoards2 is a social discussion forum for open communication and community-driven conversations.\n\nUsers can start discussions by creating or reposting threads and then submitting comments or replies to\nother user comments.\n\nDiscussions happen within different boards, where each board is an independent self managed community.\n\nBoards2 allows users to create two types of boards, one is the invite only board where only invited users\ncan create threads and comment, and where non invited users can only read the content and discussions; The\nother type of board is the open board where any user with a specific amount of GNOT in their account can\ncreate threads, repost and comment.\n\n## Open Boards Quick Start\n\nIf you are new to Gno.land in general, the quick start guide below can help you get started.\n\nWhat you need to create threads and start commenting within open boards is having a specific amount of GNOT\nin your Gno.land user account, which by default initially is 3000 GNOT. This initial GNOT amount could be\nchanged over time to a different amount, so this requirement can change.\n\n### How To Get a Gno.land Address\n\nTo use Boards2 you'll need a Gno.land address. You can quickly setup your account using [Adena] or any\nGno.land compatible wallet by following these steps:\n\n- Download [Adena], or a Gno.land compatible wallet\n- Once installed, you have to create a new account or add an existing one following wallet's instructions\n- If you don't have GNOT you will need to use a faucet to get some, if the network allows it\n\nFor testing networks you can use the official [Faucet Hub] to receive GNOT in your account.\n\n### How to Start Using Open Boards\n\nOnce you have the required GNOT amount in your account you can start commenting, creating and reposting\nthreads within any open board.\n\nTo comment and engage on an open board discussion visit a thread and click on the \"Comment\" link. You can\nalso reply to any of the thread's comments by clicking on the \"Reply\" link.\n\nTo create threads, visit an open board and then click on the \"Create Thread\" link, there you will have to\nenter a title and some content for the thread body.\n\nThread and comments content can be written as plaintext, or Markdown if you want to format the content so\nit's rendered as rich text.\n\nYou can also repost any thread, even the ones from invite only boards, into any open board. To do so visit\nthe thread you want to repost and click on the \"Repost\" link at the bottom of the thread, there you will have\nto enter the open board where you want the repost to be created, a title for the thread repost and optionally\nalso some content to render at the top of the repost. The optional content can also be written as plaintext\nor Markdown, like threads.\n\nAfter your thread, repost or comment is created, you can easily share the link with others so they can join\nthe discussion!\n\n## Boards\n\nBoards2 realm enables the creation of different communities though independent boards.\n\nWhen a board is created, and independetly of the board type, it initially has a single \"owner\" member\nassigned by default, which is the user that creates it. The member is called \"owner\" because by default it\nhas the `owner` role, which grants all permissions within that board.\n\nMembers of a board with the `owner` or `admin` role, independently of the board type, can invite other\nmembers, or otherwise users can request being invited to be a member by visiting the board and clicking the\n\"Request Invite\" link. Requested invites can be accepted or revoked though the board's \"Invite Requests\" view\nor using these public realm functions:\n\n```go\n// AcceptInvite accepts a board invite request.\nfunc AcceptInvite(_ realm, boardID boards.ID, user address)\n\n// RevokeInvite revokes a board invite request\nfunc RevokeInvite(_ realm, boardID boards.ID, user address)\n```\n\nThere are four possible roles that invited users can have when they are members of a board:\n- `owner`: Grants all available permissions\n- `admin`: Grants basic, moderator and advanced permissions, like being able to rename boards, add or remove\n   members, or change their role.\n- `moderator`: Grants basic and moderation related permissions, like being able to ban or unban users, or\n  flag content.\n- `guest`: Grants basic permissions that allow creating threads, reposting and commenting.\n\nDefault board configuration, permissions and roles are defined in the [permissions file].\n\nNo roles or number of members is enforced for boards, so technically a board can be updated to have no\nmembers, or for example, boards could exists without any \"owner\" if all members with `owner` role are removed\nfrom it.\n\nOther custom user defined roles can exists on top of the default ones though [custom board] implementations.\n\n### Custom Boards\n\nBoards2 realm allows users to customize the mechanics of their boards when the default ones doesn't make\nsense to that community, or when users want to integrate a board with their realms.\n\nAn example of this would be a case where thread creation should be allowed only though a new `publisher`\nrole, or a case where a community have their own DAO realm and governance implementation and are looking to\nintegrate it into their board mechanics by creating threads though proposals that must be approved for the\nthread to be published.\n\nEach board can customize the way it works by implementing the [Permissions] interface that is defined in the\n[gno.land/p/gnoland/boards] package. It is though the implementation of that interface within a new realm\nthat the default board mechanics can be customized. The new realm can then be used to create an instance of\na custom `Permissions` implementation to replace the one assigned by default to a board.\n\nRight now only Boards2 realm `owner` members are allowed to change default board permissions using a public\nrealm function:\n\n```go\n// SetPermissions sets a permissions implementation for boards2 realm or a board\nfunc SetPermissions(_ realm, boardID boards.ID, p boards.Permissions)\n```\n\n\u003e This function will be replaced by a proposal that would need to pass for the custom permissions to be\n\u003e applied to a board once Boards2 governance is implemented.\n\n`Permissions` implementation allow communities to customize the way they want to manage users and roles,\nwhere or how they should be stored, and the requirements or effects different board actions have.\n\nBoards2 provides a custom `Permissions` implementation in [gno.land/r/gnoland/boards2/v1/permissions] that\ncan be imported by realms and used to implement custom boards.\n\n### Boards Governance\n\nBy default boards are created with an undelying DAO, so each new board is linked to an independent DAO which\nis used to organize members by role, and can also be used to update boards in a permissionless manner.\n\nRight now is possible to integrate with the underlying DAO and change the default board mechanics to rely on\nproposals using a [custom board] implementation, by creating a new realm that imports and uses the\n[gno.land/r/gnoland/boards2/v1/permissions] realm, which exposes the underlying DAO.\n\n\u003e Current Boards2 realm implementation doesn't run proposals, but some of the current mechanics will rely on\n\u003e DAO proposals to actually execute changes.\n\n## Moderation\n\n### Flagging\n\nThreads and comments are moderated by flagging, which requires the `moderator`, `admin` or `owner` roles.\n\nA reason is required each time content is flagged by a member. Content is replaced by a feedback message\nand a link to the list of flagging reasons given by moderators when a moderation flagging threshold is\nreached. By default the threshold is of a single flag.\n\n\u003e Right now is not possible to show the content of a thread or comment that has been hidden because of\n\u003e moderation, but future Boards2 versions might implement a way to handle moderation disputes and allow\n\u003e restoring the thread or comment content.\n\n\u003e Boards2 realm `owners` are allowed to moderate content with a single flag within any board at this point,\n\u003e but this might be changed to work though a DAO proposal.\n\nEach board's `owner` or `admin` members are free to change the flagging threshold within a single board to a\ngreater value using a public realm function:\n\n```go\n// SetFlaggingThreshold sets the number of flags required to hide a thread or comment\nfunc SetFlaggingThreshold(_ realm, boardID boards.ID, threshold int)\n```\n\n### Banning\n\nMembers with the `moderator`, `admin` or `owner` roles are the only ones that are allowed to ban or unban\na user within a board.\n\nUsers can be banned with a reason for any number of hours. Within this period banned users are not allowed\nto interact or make any changes.\n\nOnly invited `guest` members and open board users can be banned, banning board owners, admins and moderators\nis not allowed.\n\nBanning and unbanning can be done by calling these public realm functions:\n\n```go\n// Ban bans a user from a board for a period of time\nfunc Ban(_ realm, boardID boards.ID, user address, hours uint, reason string)\n\n// Unban unbans a user from a board\nfunc Unban(_ realm, boardID boards.ID, user address, reason string)\n```\n\n## Freezing\n\nBoards2 realm allows `owner` or `admin` members of a board to freeze the board or any of its threads.\nFreezing makes the board or thread readonly, disallowing any changes or additions until unfrozen.\n\nThe following public realm function can be called for freezing:\n\n```go\n// FreezeBoard freezes a board so no more threads and comments can be created or modified\nfunc FreezeBoard(_ realm, boardID boards.ID)\n\n// UnfreezeBoard removes frozen status from a board\nfunc UnfreezeBoard(_ realm, boardID boards.ID)\n\n// FreezeThread freezes a thread so thread cannot be replied, modified or deleted\nfunc FreezeThread(_ realm, boardID, threadID boards.ID)\n\n// UnfreezeThread removes frozen status from a thread\nfunc UnfreezeThread(_ realm, boardID, threadID boards.ID)\n```\n\n\n[permissions file]: https://gno.land/r/gnoland/boards2/v1$source\u0026file=permissions.gno\n[gno.land/r/gnoland/boards2/v1/permissions]: https://gno.land/r/gnoland/boards2/v1/permissions/\n[custom board]: #custom-boards\n[Adena]: https://www.adena.app/\n[Faucet Hub]: https://faucet.gno.land/\n[gno.land/p/gnoland/boards]: https://gno.land/p/gnoland/boards\n[Permissions]: https://gno.land/p/gnoland/boards$source\u0026file=permissions.gno#L23\n"},{"name":"boards.gno","body":"package boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\trealmPkgPath = \"gno.land/r/gnoland/boards2/v1\"\n\tgRealmPath   = \"/r/gnoland/boards2/v1\"\n)\n\nvar (\n\t// RealmLink contains Boards2 realm link.\n\t// It can be used to generate board TX links from other realms.\n\tRealmLink = txlink.Realm(realmPkgPath)\n\n\t// RequiredAccountAmount contains the required account amount for open board interactions.\n\t// The amount requirement is not applied to members that were invited to an open board.\n\t// Amount is defined as ugnot.\n\tRequiredAccountAmount = int64(3_000_000_000)\n\n\t// Notice contains an optional message that is displayed globally within the realm.\n\tNotice string\n\n\t// Help contains optional Markdown with Boards2 realm help.\n\tHelp string\n)\n\n// TODO: Refactor globals in favor of a cleaner pattern\nvar (\n\tgListedBoardsByID bptree.BPTree // string(id) -\u003e *boards.Board\n\tgInviteRequests   bptree.BPTree // string(board id) -\u003e *bptree.BPTree(address -\u003e time.Time)\n\tgBannedUsers      bptree.BPTree // string(board id) -\u003e *bptree.BPTree(address -\u003e time.Time)\n\tgLocked           struct {\n\t\trealm        bool\n\t\trealmMembers bool\n\t}\n)\n\nvar (\n\tgBoards         = boards.NewStorage()\n\tgBoardsSequence = boards.NewIdentifierGenerator()\n\tgPerms          = initRealmPermissions(\n\t\t\t\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\",\n\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", // test1\n\t) // govdao t1 multisig\n)\n\n// initRealmPermissions returns the default realm permissions.\nfunc initRealmPermissions(owners ...address) boards.Permissions {\n\tperms := permissions.New(\n\t\tpermissions.UseSingleUserRole(),\n\t\tpermissions.WithSuperRole(RoleOwner),\n\t)\n\tperms.AddRole(RoleAdmin, PermissionBoardCreate)\n\tfor _, owner := range owners {\n\t\tperms.SetUserRoles(owner, RoleOwner)\n\t}\n\n\tperms.ValidateFunc(PermissionBoardCreate, validateBasicBoardCreate)\n\tperms.ValidateFunc(PermissionMemberInvite, validateBasicMemberInvite)\n\tperms.ValidateFunc(PermissionRoleChange, validateBasicRoleChange)\n\treturn perms\n}\n\n// getInviteRequests returns invite requests for a board.\nfunc getInviteRequests(boardID boards.ID) (_ *bptree.BPTree, found bool) {\n\tv := gInviteRequests.Get(boardID.Key())\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\treturn v.(*bptree.BPTree), true\n}\n\n// getBannedUsers returns banned users within a board.\nfunc getBannedUsers(boardID boards.ID) (_ *bptree.BPTree, found bool) {\n\tv := gBannedUsers.Get(boardID.Key())\n\tif v == nil {\n\t\treturn nil, false\n\t}\n\treturn v.(*bptree.BPTree), true\n}\n\n// mustGetBoardByName returns a board or panics when it's not found.\nfunc mustGetBoardByName(name string) *boards.Board {\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tpanic(\"board does not exist with name: \" + name)\n\t}\n\treturn board\n}\n\n// mustGetBoard returns a board or panics when it's not found.\nfunc mustGetBoard(id boards.ID) *boards.Board {\n\tboard, found := gBoards.Get(id)\n\tif !found {\n\t\tpanic(\"board does not exist with ID: \" + id.String())\n\t}\n\treturn board\n}\n\n// getThread returns a board thread.\nfunc getThread(board *boards.Board, threadID boards.ID) (*boards.Post, bool) {\n\tthread, found := board.Threads.Get(threadID)\n\tif !found {\n\t\t// When thread is not found search it within hidden threads\n\t\tmeta := board.Meta.(*BoardMeta)\n\t\tthread, found = meta.HiddenThreads.Get(threadID)\n\t}\n\treturn thread, found\n}\n\n// getReply returns a thread comment or reply.\nfunc getReply(thread *boards.Post, replyID boards.ID) (*boards.Post, bool) {\n\tmeta := thread.Meta.(*ThreadMeta)\n\treturn meta.AllReplies.Get(replyID)\n}\n\n// mustGetThread returns a thread or panics when it's not found.\nfunc mustGetThread(board *boards.Board, threadID boards.ID) *boards.Post {\n\tthread, found := getThread(board, threadID)\n\tif !found {\n\t\tpanic(\"thread does not exist with ID: \" + threadID.String())\n\t}\n\treturn thread\n}\n\n// mustGetReply returns a reply or panics when it's not found.\nfunc mustGetReply(thread *boards.Post, replyID boards.ID) *boards.Post {\n\treply, found := getReply(thread, replyID)\n\tif !found {\n\t\tpanic(\"reply does not exist with ID: \" + replyID.String())\n\t}\n\treturn reply\n}\n\nfunc mustGetPermissions(bid boards.ID) boards.Permissions {\n\tif bid != 0 {\n\t\tboard := mustGetBoard(bid)\n\t\treturn board.Permissions\n\t}\n\treturn gPerms\n}\n\nfunc parseRealmPath(path string) *realmpath.Request {\n\t// Make sure request is using current realm path so paths can be parsed during Render\n\tr := realmpath.Parse(path)\n\tr.Realm = string(RealmLink)\n\treturn r\n}\n"},{"name":"doc.gno","body":"// Boards2 is a social discussion forum for open communication and community-driven conversations.\n//\n// Users can start discussions by creating or reposting threads and then submitting comments or replies\n// to other user comments.\n//\n// Discussions happen within different boards, where each board is an independent self managed community.\n//\n// Boards2 allows users to create two types of boards:\n// - Invite Only: Only invited users (members) can create threads, reposts and comments.\n// - Open: Anyone with a specific amount of GNOT in their account can create threads, reposts and comments.\npackage boards2\n"},{"name":"flag.gno","body":"package boards2\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\n// DefaultFlaggingThreshold defines the default number of flags that hides flaggable items.\nconst DefaultFlaggingThreshold = 1\n\nvar gFlaggingThresholds bptree.BPTree // string(board ID) -\u003e int\n\n// flagItem adds a flag to a post.\n// Returns whether flag count threshold is reached and post can be hidden.\n// Panics if flag count threshold was already reached.\nfunc flagItem(post *boards.Post, user address, reason string, threshold int) bool {\n\tif post.Flags.Size() \u003e= threshold {\n\t\tpanic(\"flag count threshold exceeded: \" + strconv.Itoa(threshold))\n\t}\n\n\tif post.Flags.Exists(user) {\n\t\tpanic(\"post has been already flagged by \" + user.String())\n\t}\n\n\tpost.Flags.Add(boards.Flag{\n\t\tUser:   user,\n\t\tReason: reason,\n\t})\n\n\treturn post.Flags.Size() == threshold\n}\n\nfunc getFlaggingThreshold(bid boards.ID) int {\n\tif v := gFlaggingThresholds.Get(bid.String()); v != nil {\n\t\treturn v.(int)\n\t}\n\treturn DefaultFlaggingThreshold\n}\n"},{"name":"format.gno","body":"package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/markdown/foreign/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nconst dateFormat = \"2006-01-02 3:04pm MST\"\n\nfunc padLeft(s string, length int) string {\n\tif len(s) \u003e= length {\n\t\treturn s\n\t}\n\treturn strings.Repeat(\" \", length-len(s)) + s\n}\n\nfunc padZero(u64 uint64, length int) string {\n\ts := strconv.Itoa(int(u64))\n\tif len(s) \u003e= length {\n\t\treturn s\n\t}\n\treturn strings.Repeat(\"0\", length-len(s)) + s\n}\n\nfunc indentBody(indent string, body string) string {\n\tvar (\n\t\tres   string\n\t\tlines = strings.Split(body, \"\\n\")\n\t)\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\t// Add two spaces to keep newlines within Markdown\n\t\t\tres += \"  \\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// indentForeignBody is the single \"render a user body\" operation: it\n// sandboxes the body in a \u003cgno-foreign\u003e block, indents it like\n// indentBody, AND charges one unit against the per-render budget — so\n// wrapping and budget-accounting can't drift apart (the *int signals\n// the mutation). The body renders inside the sandbox: its block\n// structure (headings, blockquotes, lists, columns, alerts) is\n// contained and cannot hijack realm chrome, so boards no longer needs\n// the write-time markdown blacklist. The opener survives the \"\u003e \"\n// comment indentation at any depth (goldmark strips the \"\u003e \" prefix\n// before the block parser runs).\nfunc indentForeignBody(indent, body string, budget *int) string {\n\t*budget-- // one \u003cgno-foreign\u003e block\n\treturn indentBody(indent, foreign.Foreign(body))\n}\n\nfunc summaryOf(text string, length int) string {\n\tlines := strings.SplitN(text, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc userLink(addr address) string {\n\tif u := users.ResolveAddress(addr); u != nil {\n\t\treturn md.UserLink(u.Name())\n\t}\n\treturn md.UserLink(addr.String())\n}\n\nfunc getRoleBadge(post *boards.Post) string {\n\tif post == nil || post.Board == nil || post.Board.Permissions == nil {\n\t\treturn \"\"\n\t}\n\n\tperms := post.Board.Permissions\n\tcreator := post.Creator\n\n\t// Check roles in order of priority\n\tif perms.HasRole(creator, RoleOwner) {\n\t\treturn \" `owner`\"\n\t}\n\tif perms.HasRole(creator, RoleAdmin) {\n\t\treturn \" `admin`\"\n\t}\n\tif perms.HasRole(creator, RoleModerator) {\n\t\treturn \" `mod`\"\n\t}\n\treturn \"\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/boards2/v1\"\ngno = \"0.9\"\n"},{"name":"hub.gno","body":"// The `hub.gno` file exposes safe, read-only views over the realm's\n// persistent state.\n//\n// Note for future maintainers: these reads perform no caller\n// authorization, so do not graft user-identity gating onto them.\n\npackage boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n\thubexts \"gno.land/p/gnoland/boards/exts/hub\"\n)\n\n// Safe view types are defined in the hub extensions package and\n// re-exported here so callers can keep using them via this realm.\ntype (\n\tBoard   = hubexts.Board\n\tComment = hubexts.Comment\n\tFlag    = hubexts.Flag\n\tMember  = hubexts.Member\n\tThread  = hubexts.Thread\n)\n\n// GetBoard returns a safe board.\nfunc GetBoard(id uint64) (Board, bool) {\n\tb, found := gBoards.Get(boards.ID(id))\n\tif !found {\n\t\treturn Board{}, false\n\t}\n\treturn hubexts.NewSafeBoard(b), true\n}\n\n// GetThread returns a safe board thread.\nfunc GetThread(boardID, threadID uint64) (Thread, bool) {\n\tt, found := getBoardThread(boardID, threadID)\n\tif !found {\n\t\treturn Thread{}, false\n\t}\n\treturn hubexts.NewSafeThread(t), true\n}\n\n// GetComment returns a safe thread comment or reply.\n// `commentID` can be the ID of a top level comment or of a nested reply.\nfunc GetComment(boardID, threadID, commentID uint64) (Comment, bool) {\n\tc, found := getComment(boardID, threadID, commentID)\n\tif !found {\n\t\treturn Comment{}, false\n\t}\n\treturn hubexts.NewSafeComment(c), true\n}\n\n// GetBoards returns a list with all boards.\n// To reverse iterate use a negative count.\nfunc GetBoards(start, count int) []Board {\n\tvar boards_ []Board\n\tgBoards.Iterate(start, count, func(b *boards.Board) bool {\n\t\tboards_ = append(boards_, hubexts.NewSafeBoard(b))\n\t\treturn false\n\t})\n\treturn boards_\n}\n\n// GetThreads returns a list with threads of a board.\n// To reverse iterate use a negative count.\n// A board without thread storage has no threads, so nil is returned.\nfunc GetThreads(boardID uint64, start, count int) []Thread {\n\tb, found := gBoards.Get(boards.ID(boardID))\n\tif !found || b.Threads == nil {\n\t\treturn nil\n\t}\n\n\tvar threads []Thread\n\tb.Threads.Iterate(start, count, func(thread *boards.Post) bool {\n\t\tthreads = append(threads, hubexts.NewSafeThread(thread))\n\t\treturn false\n\t})\n\treturn threads\n}\n\n// GetMembers returns a list with the members of a board.\n// A zero `boardID` refers to the realm, so the realm admin users are returned.\n// A non permissioned board has no members, so nil is returned.\nfunc GetMembers(boardID uint64, start, count int) []Member {\n\tperms := gPerms\n\tif boardID != 0 {\n\t\tb, found := gBoards.Get(boards.ID(boardID))\n\t\tif !found {\n\t\t\treturn nil\n\t\t}\n\t\tperms = b.Permissions\n\t}\n\n\tif perms == nil {\n\t\treturn nil\n\t}\n\n\tvar members []Member\n\tperms.IterateUsers(start, count, func(u boards.User) bool {\n\t\tmembers = append(members, hubexts.NewSafeMember(u))\n\t\treturn false\n\t})\n\treturn members\n}\n\n// GetReposts returns a list with repost of a board thread.\n// To reverse iterate use a negative count.\n// A repost is not included when its destination thread has been deleted,\n// so the total accessible results can be shorter than the thread's RepostCount(),\n// and a single call can return fewer than count results.\n// (The reason for the discrepancy is that the start index can be large,\n// and this function cannot scan all reposts up to the start index to\n// resolve the discrepancy.)\nfunc GetReposts(boardID, threadID uint64, start, count int) []Thread {\n\tt, found := getBoardThread(boardID, threadID)\n\tif !found {\n\t\treturn nil\n\t}\n\n\tvar reposts []Thread\n\tt.Reposts.Iterate(start, count, func(rBoardID, rRepostID boards.ID) bool {\n\t\tr, found := getBoardThread(uint64(rBoardID), uint64(rRepostID))\n\t\tif found {\n\t\t\treposts = append(reposts, hubexts.NewSafeThread(r))\n\t\t}\n\t\treturn false\n\t})\n\treturn reposts\n}\n\n// GetFlags returns a list with thread or comment moderation flags.\n// To reverse iterate use a negative count.\n// Thread flags are returned when `commentID` is zero, or the flags of the\n// comment or reply with that ID are returned otherwise.\nfunc GetFlags(boardID, threadID, commentID uint64, start, count int) []Flag {\n\tvar storage boards.FlagStorage\n\tif commentID == 0 {\n\t\tt, found := getBoardThread(boardID, threadID)\n\t\tif !found {\n\t\t\treturn nil\n\t\t}\n\n\t\tstorage = t.Flags\n\t} else {\n\t\tc, found := getComment(boardID, threadID, commentID)\n\t\tif !found {\n\t\t\treturn nil\n\t\t}\n\n\t\tstorage = c.Flags\n\t}\n\n\tvar flags []Flag\n\tstorage.Iterate(start, count, func(f boards.Flag) bool {\n\t\tflags = append(flags, hubexts.NewSafeFlag(f))\n\t\treturn false\n\t})\n\treturn flags\n}\n\n// GetComments returns a list with all thread comments and replies.\n// To reverse iterate use a negative count.\n// Top level comments can be filtered by comparing `Comment.ParentID()` to\n// `Comment.ThreadID()`: a top level comment's parent is its thread, while a\n// reply's parent is the comment or reply it answers.\nfunc GetComments(boardID, threadID uint64, start, count int) []Comment {\n\tt, found := getBoardThread(boardID, threadID)\n\tif !found {\n\t\treturn nil\n\t}\n\n\tvar comments []Comment\n\tt.Replies.Iterate(start, count, func(comment *boards.Post) bool {\n\t\tcomments = append(comments, hubexts.NewSafeComment(comment))\n\t\treturn false\n\t})\n\treturn comments\n}\n\n// GetReplies returns a list with the direct replies of a comment or reply.\n// To reverse iterate use a negative count.\n// `commentID` can be the ID of a top level comment or of a nested reply.\nfunc GetReplies(boardID, threadID, commentID uint64, start, count int) []Comment {\n\tc, found := getComment(boardID, threadID, commentID)\n\tif !found {\n\t\treturn nil\n\t}\n\n\tvar replies []Comment\n\tc.Replies.Iterate(start, count, func(comment *boards.Post) bool {\n\t\treplies = append(replies, hubexts.NewSafeComment(comment))\n\t\treturn false\n\t})\n\treturn replies\n}\n\n// getBoardThread returns a board thread from their IDs.\nfunc getBoardThread(boardID, threadID uint64) (*boards.Post, bool) {\n\tb, found := gBoards.Get(boards.ID(boardID))\n\tif !found {\n\t\treturn nil, false\n\t}\n\treturn getThread(b, boards.ID(threadID))\n}\n\n// getComment returns a thread comment or reply from their IDs.\n// It searches the thread's flat index of all comments and replies, so\n// nested replies are addressable by ID and not only top level comments.\nfunc getComment(boardID, threadID, commentID uint64) (*boards.Post, bool) {\n\tt, found := getBoardThread(boardID, threadID)\n\tif !found {\n\t\treturn nil, false\n\t}\n\n\treturn getReply(t, boards.ID(commentID))\n}\n"},{"name":"meta.gno","body":"package boards2\n\nimport \"gno.land/p/gnoland/boards\"\n\n// BoardMeta defines a type for board metadata.\ntype BoardMeta struct {\n\t// HiddenThreads contains hidden board threads.\n\tHiddenThreads boards.PostStorage\n}\n\n// ThreadMeta defines a type for thread metadata.\ntype ThreadMeta struct {\n\t// AllReplies contains all existing thread comments and replies.\n\tAllReplies boards.PostStorage\n}\n"},{"name":"permissions.gno","body":"package boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n)\n\n// List of Boards2 member roles.\nconst (\n\tRoleOwner     boards.Role = \"owner\"\n\tRoleAdmin                 = \"admin\"\n\tRoleModerator             = \"moderator\"\n\tRoleGuest                 = \"guest\"\n)\n\n// PermissionCustom defines an initial value for custom board permissions.\n// When a board defines custom permissions it must starts from a value\n// greater or equal than PermissionCustom.\n//\n// Custom permissions definition example:\n//\n//\tconst (\n//\t  PermissionCustom1 boards.Permission = iota + boards2.PermissionCustom\n//\t  PermissionCustom2\n//\t  PermissionCustom3\n//\t)\nconst PermissionCustom boards.Permission = 200\n\n// List of Boards2 permissions.\nconst (\n\tPermissionBoardCreate boards.Permission = iota\n\tPermissionBoardFlaggingUpdate\n\tPermissionBoardFreeze\n\tPermissionBoardRename\n\tPermissionMemberInvite\n\tPermissionMemberInviteRevoke\n\tPermissionMemberRemove\n\tPermissionPermissionsUpdate\n\tPermissionRealmHelpChange\n\tPermissionRealmLock\n\tPermissionRealmNotice\n\tPermissionAccountRequiredAmountChange\n\tPermissionReplyCreate\n\tPermissionReplyDelete\n\tPermissionReplyFlag\n\tPermissionRoleChange\n\tPermissionThreadCreate\n\tPermissionThreadDelete\n\tPermissionThreadEdit\n\tPermissionThreadFlag\n\tPermissionThreadFreeze\n\tPermissionThreadRepost\n\tPermissionUserBan\n\tPermissionUserUnban\n)\n\nfunc createBasicBoardPermissions(owner address) *permissions.Permissions {\n\tperms := permissions.New(\n\t\tpermissions.UseSingleUserRole(),\n\t\tpermissions.WithSuperRole(RoleOwner),\n\t)\n\tperms.AddRole(\n\t\tRoleAdmin,\n\t\tPermissionBoardRename,\n\t\tPermissionBoardFlaggingUpdate,\n\t\tPermissionMemberInvite,\n\t\tPermissionMemberInviteRevoke,\n\t\tPermissionMemberRemove,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadEdit,\n\t\tPermissionThreadDelete,\n\t\tPermissionThreadRepost,\n\t\tPermissionThreadFlag,\n\t\tPermissionThreadFreeze,\n\t\tPermissionReplyCreate,\n\t\tPermissionReplyDelete,\n\t\tPermissionReplyFlag,\n\t\tPermissionRoleChange,\n\t\tPermissionUserBan,\n\t\tPermissionUserUnban,\n\t)\n\tperms.AddRole(\n\t\tRoleModerator,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadEdit,\n\t\tPermissionThreadRepost,\n\t\tPermissionThreadFlag,\n\t\tPermissionReplyCreate,\n\t\tPermissionReplyFlag,\n\t\tPermissionUserBan,\n\t\tPermissionUserUnban,\n\t)\n\tperms.AddRole(\n\t\tRoleGuest,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadRepost,\n\t\tPermissionReplyCreate,\n\t)\n\tperms.SetUserRoles(owner, RoleOwner)\n\tperms.ValidateFunc(PermissionBoardRename, validateBasicBoardRename)\n\tperms.ValidateFunc(PermissionMemberInvite, validateBasicMemberInvite)\n\tperms.ValidateFunc(PermissionRoleChange, validateBasicRoleChange)\n\treturn perms\n}\n\nfunc createOpenBoardPermissions(owner address) *permissions.Permissions {\n\tperms := permissions.New(\n\t\tpermissions.UseSingleUserRole(),\n\t\tpermissions.WithSuperRole(RoleOwner),\n\t)\n\tperms.SetPublicPermissions(\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadRepost,\n\t\tPermissionReplyCreate,\n\t)\n\tperms.AddRole(\n\t\tRoleAdmin,\n\t\tPermissionBoardRename,\n\t\tPermissionBoardFlaggingUpdate,\n\t\tPermissionMemberInvite,\n\t\tPermissionMemberInviteRevoke,\n\t\tPermissionMemberRemove,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadEdit,\n\t\tPermissionThreadDelete,\n\t\tPermissionThreadRepost,\n\t\tPermissionThreadFlag,\n\t\tPermissionThreadFreeze,\n\t\tPermissionReplyCreate,\n\t\tPermissionReplyDelete,\n\t\tPermissionReplyFlag,\n\t\tPermissionRoleChange,\n\t\tPermissionUserBan,\n\t\tPermissionUserUnban,\n\t)\n\tperms.AddRole(\n\t\tRoleModerator,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadEdit,\n\t\tPermissionThreadRepost,\n\t\tPermissionThreadFlag,\n\t\tPermissionReplyCreate,\n\t\tPermissionReplyFlag,\n\t\tPermissionUserBan,\n\t\tPermissionUserUnban,\n\t)\n\tperms.AddRole(\n\t\tRoleGuest,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadRepost,\n\t\tPermissionReplyCreate,\n\t)\n\tperms.SetUserRoles(owner, RoleOwner)\n\tperms.ValidateFunc(PermissionBoardRename, validateOpenBoardRename)\n\tperms.ValidateFunc(PermissionMemberInvite, validateOpenMemberInvite)\n\tperms.ValidateFunc(PermissionRoleChange, validateOpenRoleChange)\n\tperms.ValidateFunc(PermissionThreadCreate, validateOpenThreadCreate)\n\tperms.ValidateFunc(PermissionReplyCreate, validateOpenReplyCreate)\n\treturn perms\n}\n"},{"name":"permissions_validators_basic.gno","body":"package boards2\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// validateBasicBoardCreate validates PermissionBoardCreate.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board name\n// 3. Board ID\n// 4. Is board listed\n// 5. Is board open\nfunc validateBasicBoardCreate(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tname, ok := args[1].(string)\n\tif !ok {\n\t\treturn errors.New(\"expected board name to be a string\")\n\t}\n\n\topen, ok := args[4].(bool)\n\tif !ok {\n\t\treturn errors.New(\"expected board open flag to be a boolean\")\n\t}\n\n\tif open \u0026\u0026 !perms.HasRole(caller, RoleOwner) {\n\t\treturn errors.New(\"only owners can create open boards\")\n\t}\n\n\tif err := checkBoardNameIsNotAddress(name); err != nil {\n\t\treturn err\n\t}\n\n\tif err := checkBoardNameBelongsToAddress(caller, name); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateBasicBoardRename validates PermissionBoardRename.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Current board name\n// 4. New board name\nfunc validateBasicBoardRename(_ boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tnewName, ok := args[3].(string)\n\tif !ok {\n\t\treturn errors.New(\"expected new board name to be a string\")\n\t}\n\n\tif err := checkBoardNameIsNotAddress(newName); err != nil {\n\t\treturn err\n\t}\n\n\tif err := checkBoardNameBelongsToAddress(caller, newName); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateBasicMemberInvite validates PermissionMemberInvite.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Invites\nfunc validateBasicMemberInvite(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tinvites, ok := args[2].([]Invite)\n\tif !ok {\n\t\treturn errors.New(\"expected valid user invites\")\n\t}\n\n\t// Make sure that only owners invite other owners\n\tcallerIsOwner := perms.HasRole(caller, RoleOwner)\n\tfor _, v := range invites {\n\t\tif v.Role == RoleOwner \u0026\u0026 !callerIsOwner {\n\t\t\treturn errors.New(\"only owners are allowed to invite other owners\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateBasicRoleChange validates PermissionRoleChange.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Member address\n// 4. Role\nfunc validateBasicRoleChange(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// Owners and Admins can change roles.\n\t// Admins should not be able to assign or remove the Owner role from members.\n\tif perms.HasRole(caller, RoleAdmin) {\n\t\trole, ok := args[3].(boards.Role)\n\t\tif !ok {\n\t\t\treturn errors.New(\"expected a valid member role\")\n\t\t}\n\n\t\tif role == RoleOwner {\n\t\t\treturn errors.New(\"admins are not allowed to promote members to Owner\")\n\t\t} else {\n\t\t\tmember, ok := args[2].(address)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"expected a valid member address\")\n\t\t\t}\n\n\t\t\tif perms.HasRole(member, RoleOwner) {\n\t\t\t\treturn errors.New(\"admins are not allowed to remove the Owner role\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkBoardNameIsNotAddress(s string) error {\n\tif address(s).IsValid() {\n\t\treturn errors.New(\"addresses are not allowed as board name\")\n\t}\n\treturn nil\n}\n\nfunc checkBoardNameBelongsToAddress(owner address, name string) error {\n\t// When the board name is the name of a registered user\n\t// check that caller is the owner of the name.\n\tuser, _ := users.ResolveName(name)\n\tif user != nil \u0026\u0026 user.Addr() != owner {\n\t\treturn errors.New(\"board name is a user name registered to a different user\")\n\t}\n\treturn nil\n}\n"},{"name":"permissions_validators_open.gno","body":"package boards2\n\nimport (\n\t\"chain/banker\"\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// validateOpenBoardRename validates PermissionBoardRename.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Current board name\n// 4. New board name\nfunc validateOpenBoardRename(_ boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tnewName, ok := args[3].(string)\n\tif !ok {\n\t\treturn errors.New(\"expected new board name to be a string\")\n\t}\n\n\tif err := checkBoardNameIsNotAddress(newName); err != nil {\n\t\treturn err\n\t}\n\n\tif err := checkBoardNameBelongsToAddress(caller, newName); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateOpenMemberInvite validates PermissionMemberInvite.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Invites\nfunc validateOpenMemberInvite(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tinvites, ok := args[2].([]Invite)\n\tif !ok {\n\t\treturn errors.New(\"expected valid user invites\")\n\t}\n\n\t// Make sure that only owners invite other owners\n\tcallerIsOwner := perms.HasRole(caller, RoleOwner)\n\tfor _, v := range invites {\n\t\tif v.Role == RoleOwner \u0026\u0026 !callerIsOwner {\n\t\t\treturn errors.New(\"only owners are allowed to invite other owners\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateOpenRoleChange validates PermissionRoleChange.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Member address\n// 4. Role\nfunc validateOpenRoleChange(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// Owners and Admins can change roles.\n\t// Admins should not be able to assign or remove the Owner role from members.\n\tif perms.HasRole(caller, RoleAdmin) {\n\t\trole, ok := args[3].(boards.Role)\n\t\tif !ok {\n\t\t\treturn errors.New(\"expected a valid member role\")\n\t\t}\n\n\t\tif role == RoleOwner {\n\t\t\treturn errors.New(\"admins are not allowed to promote members to Owner\")\n\t\t} else {\n\t\t\tmember, ok := args[2].(address)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"expected a valid member address\")\n\t\t\t}\n\n\t\t\tif perms.HasRole(member, RoleOwner) {\n\t\t\t\treturn errors.New(\"admins are not allowed to remove the Owner role\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateOpenThreadCreate validates PermissionThreadCreate.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Thread ID\n// 4. Title\n// 5. Body\nfunc validateOpenThreadCreate(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// Owners and admins can create threads without special requirements\n\tif perms.HasRole(caller, RoleOwner) || perms.HasRole(caller, RoleAdmin) {\n\t\treturn nil\n\t}\n\n\t// Require non members to have some GNOT in their accounts\n\tif err := checkAccountHasAmount(caller, RequiredAccountAmount); err != nil {\n\t\treturn ufmt.Errorf(\"caller is not allowed to create threads: %s\", err)\n\t}\n\treturn nil\n}\n\n// validateOpenReplyCreate validates PermissionReplyCreate.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Thread ID\n// 4. Parent ID\n// 5. Reply ID\n// 6. Body\nfunc validateOpenReplyCreate(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// All board members can reply\n\tif perms.HasUser(caller) {\n\t\treturn nil\n\t}\n\n\t// Require non members to have some GNOT in their accounts\n\tif err := checkAccountHasAmount(caller, RequiredAccountAmount); err != nil {\n\t\treturn ufmt.Errorf(\"caller is not allowed to comment: %s\", err)\n\t}\n\treturn nil\n}\n\nfunc checkAccountHasAmount(addr address, amount int64) error {\n\tbnk := banker.NewReadonlyBanker()\n\tif bnk.GetCoin(addr, \"ugnot\") \u003c RequiredAccountAmount {\n\t\tamount = amount / 1_000_000 // ugnot -\u003e GNOT\n\t\treturn ufmt.Errorf(\"account amount is lower than %d GNOT\", amount)\n\t}\n\treturn nil\n}\n"},{"name":"public.gno","body":"package boards2\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\nconst (\n\t// MaxBoardNameLength defines the maximum length allowed for board names.\n\tMaxBoardNameLength = 50\n\n\t// MaxThreadTitleLength defines the maximum length allowed for thread titles.\n\tMaxThreadTitleLength = 100\n\n\t// MaxThreadBodyLength defines the maximum length allowed for thread bodies.\n\t// 40,000 mirrors Reddit's self-post body cap.\n\tMaxThreadBodyLength = 40000\n\n\t// MaxReplyLength defines the maximum length allowed for replies.\n\t// 10,000 mirrors Reddit's comment cap.\n\tMaxReplyLength = 10000\n)\n\nvar reBoardName = regexp.MustCompile(`(?i)^[a-z]+[a-z0-9_\\-]{2,50}$`)\n\n// SetHelp sets or updates boards realm help content.\nfunc SetHelp(cur realm, content string) {\n\tcontent = strings.TrimSpace(content)\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{content}\n\tgPerms.WithPermission(caller, PermissionRealmHelpChange, args, func() {\n\t\tHelp = content\n\t})\n}\n\n// SetRequiredAccountAmount sets the required account amount to interact as a non member with open boards.\n// Amount must be given as ugnot.\n// The amount requirement is not applied to members that were invited to an open board.\nfunc SetRequiredAccountAmount(cur realm, amount int64) {\n\tif amount \u003c 0 {\n\t\tpanic(\"invalid amount\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{amount}\n\tgPerms.WithPermission(caller, PermissionAccountRequiredAmountChange, args, func() {\n\t\tRequiredAccountAmount = amount\n\n\t\tchain.Emit(\n\t\t\t\"RequiredAccountAmountChanged\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"amount\", strconv.FormatInt(amount, 10),\n\t\t)\n\t})\n}\n\n// SetPermissions sets a permissions implementation for boards2 realm or a board.\nfunc SetPermissions(cur realm, boardID boards.ID, p boards.Permissions) {\n\tassertRealmIsNotLocked()\n\tassertBoardExists(boardID)\n\n\tif p == nil {\n\t\tpanic(\"permissions is required\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{boardID}\n\tgPerms.WithPermission(caller, PermissionPermissionsUpdate, args, func() {\n\t\tassertRealmIsNotLocked()\n\n\t\t// When board ID is zero it means that realm permissions are being updated\n\t\tif boardID == 0 {\n\t\t\tgPerms = p\n\n\t\t\tchain.Emit(\n\t\t\t\t\"RealmPermissionsChanged\",\n\t\t\t\t\"caller\", caller.String(),\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\t// Otherwise update the permissions of a single board\n\t\tboard := mustGetBoard(boardID)\n\t\tboard.Permissions = p\n\n\t\tchain.Emit(\n\t\t\t\"BoardPermissionsChanged\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t)\n\t})\n}\n\n// SetRealmNotice sets a notice to be displayed globally within the realm.\n// An empty message removes the realm notice.\nfunc SetRealmNotice(cur realm, message string) {\n\tmessage = strings.TrimSpace(message)\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{message}\n\tgPerms.WithPermission(caller, PermissionRealmNotice, args, func() {\n\t\tNotice = message\n\n\t\tchain.Emit(\n\t\t\t\"RealmNoticeChanged\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"message\", message,\n\t\t)\n\t})\n}\n\n// GetBoardIDFromName searches a board by name and returns its ID.\nfunc GetBoardIDFromName(_ realm, name string) (_ boards.ID, found bool) {\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\treturn 0, false\n\t}\n\treturn board.ID, true\n}\n\n// BoardCount returns the total number of boards.\nfunc BoardCount() int {\n\treturn gBoards.Size()\n}\n\n// CreateBoard creates a new board.\n//\n// Listed boards are included in the realm's list of boards.\n// Open boards allow anyone to create threads and comment.\nfunc CreateBoard(cur realm, name string, listed, open bool) boards.ID {\n\tassertRealmIsNotLocked()\n\n\tname = strings.TrimSpace(name)\n\tassertIsValidBoardName(name)\n\tassertBoardNameNotExists(name)\n\n\tcaller := cur.Previous().Address()\n\tid := gBoardsSequence.Next()\n\tboard := boards.New(id)\n\targs := boards.Args{caller, name, board.ID, listed, open}\n\tgPerms.WithPermission(caller, PermissionBoardCreate, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardNameNotExists(name)\n\n\t\tboard.Name = name\n\t\tboard.Creator = caller\n\t\tboard.Meta = \u0026BoardMeta{\n\t\t\tHiddenThreads: boards.NewPostStorage(),\n\t\t}\n\n\t\tif open {\n\t\t\tboard.Permissions = createOpenBoardPermissions(caller)\n\t\t} else {\n\t\t\tboard.Permissions = createBasicBoardPermissions(caller)\n\t\t}\n\n\t\tif err := gBoards.Add(board); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// Listed boards are also indexed separately for easier iteration and pagination\n\t\tif listed {\n\t\t\tgListedBoardsByID.Set(board.ID.Key(), board)\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"BoardCreated\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"name\", name,\n\t\t)\n\t})\n\treturn board.ID\n}\n\n// RenameBoard changes the name of an existing board.\n//\n// A history of previous board names is kept when boards are renamed.\n// Because of that boards are also accessible using previous name(s).\nfunc RenameBoard(cur realm, name, newName string) {\n\tassertRealmIsNotLocked()\n\n\tnewName = strings.TrimSpace(newName)\n\tassertIsValidBoardName(newName)\n\tassertBoardNameNotExists(newName)\n\n\tboard := mustGetBoardByName(name)\n\tassertBoardIsNotFrozen(board)\n\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{caller, board.ID, name, newName}\n\tboard.Permissions.WithPermission(caller, PermissionBoardRename, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardNameNotExists(newName)\n\n\t\tboard.Aliases = append(board.Aliases, board.Name)\n\t\tboard.Name = newName\n\t\tboard.UpdatedAt = time.Now()\n\n\t\t// Index board for the new name keeping previous indexes for older names\n\t\tgBoards.Add(board)\n\n\t\tchain.Emit(\n\t\t\t\"BoardRenamed\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"name\", name,\n\t\t\t\"newName\", newName,\n\t\t)\n\t})\n}\n\n// CreateThread creates a new thread within a board.\nfunc CreateThread(cur realm, boardID boards.ID, title, body string) boards.ID {\n\tassertRealmIsNotLocked()\n\n\ttitle = strings.TrimSpace(title)\n\tassertTitleIsValid(title)\n\tassertThreadBodyIsValid(body)\n\n\tcaller := cur.Previous().Address()\n\tassertUserIsNotBanned(boardID, caller)\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tthread := boards.MustNewThread(board, caller, title, body)\n\targs := boards.Args{caller, board.ID, thread.ID, title, body}\n\tboard.Permissions.WithPermission(caller, PermissionThreadCreate, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\tassertUserIsNotBanned(board.ID, caller)\n\n\t\tthread.Meta = \u0026ThreadMeta{\n\t\t\tAllReplies: boards.NewPostStorage(),\n\t\t}\n\n\t\tif err := board.Threads.Add(thread); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"ThreadCreated\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"title\", title,\n\t\t)\n\t})\n\treturn thread.ID\n}\n\n// CreateReply creates a new comment or reply within a thread.\n//\n// The value of `replyID` is only required when creating a reply of another reply.\nfunc CreateReply(cur realm, boardID, threadID, replyID boards.ID, body string) boards.ID {\n\tassertRealmIsNotLocked()\n\n\tbody = strings.TrimSpace(body)\n\tassertReplyBodyIsValid(body)\n\n\tcaller := cur.Previous().Address()\n\tassertUserIsNotBanned(boardID, caller)\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tthread := mustGetThread(board, threadID)\n\tassertThreadIsVisible(thread)\n\tassertThreadIsNotFrozen(thread)\n\n\t// By default consider that reply's parent is the thread.\n\t// Or when replyID is assigned use that reply as the parent.\n\tparent := thread\n\tif replyID \u003e 0 {\n\t\tparent = mustGetReply(thread, replyID)\n\t\tif parent.Hidden || parent.Readonly {\n\t\t\tpanic(\"replying to a hidden or frozen reply is not allowed\")\n\t\t}\n\t}\n\n\treply := boards.MustNewReply(parent, caller, body)\n\targs := boards.Args{caller, board.ID, thread.ID, parent.ID, reply.ID, body}\n\tboard.Permissions.WithPermission(caller, PermissionReplyCreate, args, func() {\n\t\tassertRealmIsNotLocked()\n\n\t\t// Add reply to its parent\n\t\tif err := parent.Replies.Add(reply); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// Always add reply to the thread so it contains all comments and replies.\n\t\t// Comment and reply only contains direct replies.\n\t\tmeta := thread.Meta.(*ThreadMeta)\n\t\tif err := meta.AllReplies.Add(reply); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"ReplyCreate\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"replyID\", reply.ID.String(),\n\t\t)\n\t})\n\treturn reply.ID\n}\n\n// CreateRepost reposts a thread into another board.\nfunc CreateRepost(cur realm, boardID, threadID, destinationBoardID boards.ID, title, body string) boards.ID {\n\tassertRealmIsNotLocked()\n\n\ttitle = strings.TrimSpace(title)\n\tassertTitleIsValid(title)\n\tassertThreadBodyIsValid(body)\n\n\tcaller := cur.Previous().Address()\n\tassertUserIsNotBanned(destinationBoardID, caller)\n\n\tdst := mustGetBoard(destinationBoardID)\n\tassertBoardIsNotFrozen(dst)\n\n\tboard := mustGetBoard(boardID)\n\tthread := mustGetThread(board, threadID)\n\tassertThreadIsVisible(thread)\n\n\trepost := boards.MustNewRepost(thread, dst, caller)\n\targs := boards.Args{caller, board.ID, thread.ID, dst.ID, repost.ID, title, body}\n\tdst.Permissions.WithPermission(caller, PermissionThreadRepost, args, func() {\n\t\tassertRealmIsNotLocked()\n\n\t\trepost.Title = title\n\t\trepost.Body = strings.TrimSpace(body)\n\t\trepost.Meta = \u0026ThreadMeta{\n\t\t\tAllReplies: boards.NewPostStorage(),\n\t\t}\n\n\t\tif err := dst.Threads.Add(repost); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif err := thread.Reposts.Add(repost); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"Repost\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"destinationBoardID\", dst.ID.String(),\n\t\t\t\"repostID\", repost.ID.String(),\n\t\t\t\"title\", title,\n\t\t)\n\t})\n\treturn repost.ID\n}\n\n// DeleteThread deletes a thread from a board.\n//\n// Threads can be deleted by the users who created them or otherwise by users with special permissions.\nfunc DeleteThread(cur realm, boardID, threadID boards.ID) {\n\tassertRealmIsNotLocked()\n\n\tcaller := cur.Previous().Address()\n\tboard := mustGetBoard(boardID)\n\tassertUserIsNotBanned(boardID, caller)\n\n\tisRealmOwner := gPerms.HasRole(caller, RoleOwner) // TODO: Add DeleteThread filetest cases for realm owners\n\tif !isRealmOwner {\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tthread := mustGetThread(board, threadID)\n\tdeleteThread := func() {\n\t\tboard.Threads.Remove(thread.ID)\n\n\t\tchain.Emit(\n\t\t\t\"ThreadDeleted\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t)\n\t}\n\n\t// Thread can be directly deleted by user that created it.\n\t// It can also be deleted by realm owners, to be able to delete inappropriate content.\n\t// TODO: Discuss and decide if realm owners should be able to delete threads.\n\tif isRealmOwner || caller == thread.Creator {\n\t\tdeleteThread()\n\t\treturn\n\t}\n\n\targs := boards.Args{caller, board.ID, thread.ID}\n\tboard.Permissions.WithPermission(caller, PermissionThreadDelete, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\tdeleteThread()\n\t})\n}\n\n// DeleteReply deletes a reply from a thread.\n//\n// Replies can be deleted by the users who created them or otherwise by users with special permissions.\n// Soft deletion is used when the deleted reply contains sub replies, in which case the reply content\n// is replaced by a text informing that reply has been deleted to avoid deleting sub-replies.\nfunc DeleteReply(cur realm, boardID, threadID, replyID boards.ID) {\n\tassertRealmIsNotLocked()\n\n\tcaller := cur.Previous().Address()\n\tboard := mustGetBoard(boardID)\n\tassertUserIsNotBanned(boardID, caller)\n\n\tthread := mustGetThread(board, threadID)\n\treply := mustGetReply(thread, replyID)\n\tisRealmOwner := gPerms.HasRole(caller, RoleOwner) // TODO: Add DeleteReply filetest cases for realm owners\n\tif !isRealmOwner {\n\t\tassertBoardIsNotFrozen(board)\n\t\tassertThreadIsNotFrozen(thread)\n\t\tassertReplyIsVisible(reply)\n\t}\n\n\tdeleteReply := func() {\n\t\t// Soft delete comment/reply by changing its body when\n\t\t// it contains replies, otherwise hard delete it.\n\t\tif reply.Replies.Size() \u003e 0 {\n\t\t\treply.Body = \"⚠ This comment has been deleted\"\n\t\t\treply.UpdatedAt = time.Now()\n\t\t} else {\n\t\t\t// Remove reply from the flat thread index.\n\t\t\tmeta := thread.Meta.(*ThreadMeta)\n\t\t\treply, removed := meta.AllReplies.Remove(replyID)\n\t\t\tif !removed {\n\t\t\t\tpanic(\"reply not found\")\n\t\t\t}\n\n\t\t\t// Remove reply from its parent's direct-children list too. A\n\t\t\t// direct thread reply's parent is the thread itself; a nested\n\t\t\t// reply's parent is another reply (resolved via the flat index).\n\t\t\t// Missing the thread-parent case left a ghost in thread.Replies\n\t\t\t// (rendered in the threaded view, with a stale reply count).\n\t\t\tif reply.ParentID == thread.ID {\n\t\t\t\tthread.Replies.Remove(replyID)\n\t\t\t} else if parent, found := meta.AllReplies.Get(reply.ParentID); found {\n\t\t\t\tparent.Replies.Remove(replyID)\n\t\t\t}\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"ReplyDeleted\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"replyID\", reply.ID.String(),\n\t\t)\n\t}\n\n\t// Reply can be directly deleted by user that created it.\n\t// It can also be deleted by realm owners, to be able to delete inappropriate content.\n\t// TODO: Discuss and decide if realm owners should be able to delete replies.\n\tif isRealmOwner || caller == reply.Creator {\n\t\tdeleteReply()\n\t\treturn\n\t}\n\n\targs := boards.Args{caller, board.ID, thread.ID, reply.ID}\n\tboard.Permissions.WithPermission(caller, PermissionReplyDelete, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\tdeleteReply()\n\t})\n}\n\n// EditThread updates the title and body of a thread.\n//\n// Threads can be updated by the users who created them or otherwise by users with special permissions.\nfunc EditThread(cur realm, boardID, threadID boards.ID, title, body string) {\n\tassertRealmIsNotLocked()\n\n\ttitle = strings.TrimSpace(title)\n\tassertTitleIsValid(title)\n\tassertThreadBodyIsValid(body)\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tcaller := cur.Previous().Address()\n\tassertUserIsNotBanned(boardID, caller)\n\n\tthread := mustGetThread(board, threadID)\n\tassertThreadIsNotFrozen(thread)\n\n\tbody = strings.TrimSpace(body)\n\tif !boards.IsRepost(thread) {\n\t\tassertBodyIsNotEmpty(body)\n\t}\n\n\teditThread := func() {\n\t\tthread.Title = title\n\t\tthread.Body = body\n\t\tthread.UpdatedAt = time.Now()\n\n\t\tchain.Emit(\n\t\t\t\"ThreadEdited\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"title\", title,\n\t\t)\n\t}\n\n\tif caller == thread.Creator {\n\t\teditThread()\n\t\treturn\n\t}\n\n\targs := boards.Args{caller, board.ID, thread.ID, title, body}\n\tboard.Permissions.WithPermission(caller, PermissionThreadEdit, args, func() {\n\t\tassertRealmIsNotLocked()\n\t\teditThread()\n\t})\n}\n\n// EditReply updates the body of a comment or reply.\n//\n// Replies can be updated only by the users who created them.\nfunc EditReply(cur realm, boardID, threadID, replyID boards.ID, body string) {\n\tassertRealmIsNotLocked()\n\n\tbody = strings.TrimSpace(body)\n\tassertReplyBodyIsValid(body)\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tcaller := cur.Previous().Address()\n\tassertUserIsNotBanned(boardID, caller)\n\n\tthread := mustGetThread(board, threadID)\n\tassertThreadIsNotFrozen(thread)\n\n\treply := mustGetReply(thread, replyID)\n\tassertReplyIsVisible(reply)\n\n\tif caller != reply.Creator {\n\t\tpanic(\"only the reply creator is allowed to edit it\")\n\t}\n\n\treply.Body = body\n\treply.UpdatedAt = time.Now()\n\n\tchain.Emit(\n\t\t\"ReplyEdited\",\n\t\t\"caller\", caller.String(),\n\t\t\"boardID\", board.ID.String(),\n\t\t\"threadID\", thread.ID.String(),\n\t\t\"replyID\", reply.ID.String(),\n\t\t\"body\", body,\n\t)\n}\n\n// RemoveMember removes a member from the realm or a board.\n//\n// Board ID is only required when removing a member from board.\nfunc RemoveMember(cur realm, boardID boards.ID, member address) {\n\tassertMembersUpdateIsEnabled(boardID)\n\tassertMemberAddressIsValid(member)\n\n\tperms := mustGetPermissions(boardID)\n\torigin := unsafe.OriginCaller()\n\tcaller := cur.Previous().Address()\n\tremoveMember := func() {\n\t\tif !perms.RemoveUser(member) {\n\t\t\tpanic(\"member not found\")\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"MemberRemoved\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"origin\", origin.String(), // When origin and caller match it means self removal\n\t\t\t\"boardID\", boardID.String(),\n\t\t\t\"member\", member.String(),\n\t\t)\n\t}\n\n\t// Members can remove themselves without permission\n\tif origin == member {\n\t\tremoveMember()\n\t\treturn\n\t}\n\n\targs := boards.Args{boardID, member}\n\tperms.WithPermission(caller, PermissionMemberRemove, args, func() {\n\t\tassertMembersUpdateIsEnabled(boardID)\n\t\tremoveMember()\n\t})\n}\n\n// IsMember checks if a user is a member of the realm or a board.\n//\n// Board ID is only required when checking if a user is a member of a board.\nfunc IsMember(boardID boards.ID, user address) bool {\n\tassertUserAddressIsValid(user)\n\n\tif boardID != 0 {\n\t\tboard := mustGetBoard(boardID)\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tperms := mustGetPermissions(boardID)\n\treturn perms.HasUser(user)\n}\n\n// HasMemberRole checks if a realm or board member has a specific role assigned.\n//\n// Board ID is only required when checking a member of a board.\nfunc HasMemberRole(boardID boards.ID, member address, role boards.Role) bool {\n\tassertMemberAddressIsValid(member)\n\n\tif boardID != 0 {\n\t\tboard := mustGetBoard(boardID)\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tperms := mustGetPermissions(boardID)\n\treturn perms.HasRole(member, role)\n}\n\n// ChangeMemberRole changes the role of a realm or board member.\n//\n// Board ID is only required when changing the role for a member of a board.\nfunc ChangeMemberRole(cur realm, boardID boards.ID, member address, role boards.Role) {\n\tassertMemberAddressIsValid(member)\n\tassertMembersUpdateIsEnabled(boardID)\n\n\tif role == \"\" {\n\t\trole = RoleGuest\n\t}\n\n\tperms := mustGetPermissions(boardID)\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{caller, boardID, member, role}\n\tperms.WithPermission(caller, PermissionRoleChange, args, func() {\n\t\tassertMembersUpdateIsEnabled(boardID)\n\n\t\tperms.SetUserRoles(member, role)\n\n\t\tchain.Emit(\n\t\t\t\"RoleChanged\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", boardID.String(),\n\t\t\t\"member\", member.String(),\n\t\t\t\"newRole\", string(role),\n\t\t)\n\t})\n}\n\nfunc assertMemberAddressIsValid(member address) {\n\tif !member.IsValid() {\n\t\tpanic(\"invalid member address: \" + member.String())\n\t}\n}\n\nfunc assertUserAddressIsValid(user address) {\n\tif !user.IsValid() {\n\t\tpanic(\"invalid user address: \" + user.String())\n\t}\n}\n\nfunc assertBoardExists(id boards.ID) {\n\tif id == 0 { // ID zero is used to refer to the realm\n\t\treturn\n\t}\n\n\tif _, found := gBoards.Get(id); !found {\n\t\tpanic(\"board not found: \" + id.String())\n\t}\n}\n\nfunc assertBoardIsNotFrozen(b *boards.Board) {\n\tif b.Readonly {\n\t\tpanic(\"board is frozen\")\n\t}\n}\n\nfunc assertIsValidBoardName(name string) {\n\tsize := len(name)\n\tif size == 0 {\n\t\tpanic(\"board name is empty\")\n\t}\n\n\tif size \u003c 3 {\n\t\tpanic(\"board name is too short, minimum length is 3 characters\")\n\t}\n\n\tif size \u003e MaxBoardNameLength {\n\t\tn := strconv.Itoa(MaxBoardNameLength)\n\t\tpanic(\"board name is too long, maximum allowed is \" + n + \" characters\")\n\t}\n\n\tif !reBoardName.MatchString(name) {\n\t\tpanic(\"board name must start with a letter and have letters, numbers, \\\"-\\\" and \\\"_\\\"\")\n\t}\n}\n\nfunc assertThreadIsNotFrozen(t *boards.Post) {\n\tif t.Readonly {\n\t\tpanic(\"thread is frozen\")\n\t}\n}\n\nfunc assertNameIsNotEmpty(name string) {\n\tif name == \"\" {\n\t\tpanic(\"name is empty\")\n\t}\n}\n\nfunc assertTitleIsValid(title string) {\n\tif title == \"\" {\n\t\tpanic(\"title is empty\")\n\t}\n\n\tif len(title) \u003e MaxThreadTitleLength {\n\t\tn := strconv.Itoa(MaxThreadTitleLength)\n\t\tpanic(\"title is too long, maximum allowed is \" + n + \" characters\")\n\t}\n}\n\nfunc assertBodyIsNotEmpty(body string) {\n\tif body == \"\" {\n\t\tpanic(\"body is empty\")\n\t}\n}\n\nfunc assertBoardNameNotExists(name string) {\n\tname = strings.ToLower(name)\n\tif _, found := gBoards.GetByName(name); found {\n\t\tpanic(\"board already exists\")\n\t}\n}\n\nfunc assertThreadExists(b *boards.Board, threadID boards.ID) {\n\tif _, found := getThread(b, threadID); !found {\n\t\tpanic(\"thread not found: \" + threadID.String())\n\t}\n}\n\nfunc assertReplyExists(thread *boards.Post, replyID boards.ID) {\n\tif _, found := getReply(thread, replyID); !found {\n\t\tpanic(\"reply not found: \" + replyID.String())\n\t}\n}\n\nfunc assertThreadIsVisible(thread *boards.Post) {\n\tif thread.Hidden {\n\t\tpanic(\"thread is hidden\")\n\t}\n}\n\nfunc assertReplyIsVisible(thread *boards.Post) {\n\tif thread.Hidden {\n\t\tpanic(\"reply is hidden\")\n\t}\n}\n\nfunc assertThreadBodyIsValid(body string) {\n\tif len(body) \u003e MaxThreadBodyLength {\n\t\tn := strconv.Itoa(MaxThreadBodyLength)\n\t\tpanic(\"thread body is too long, maximum allowed is \" + n + \" characters\")\n\t}\n}\n\nfunc assertReplyBodyIsValid(body string) {\n\tassertBodyIsNotEmpty(body)\n\n\tif len(body) \u003e MaxReplyLength {\n\t\tn := strconv.Itoa(MaxReplyLength)\n\t\tpanic(\"reply is too long, maximum allowed is \" + n + \" characters\")\n\t}\n\n\t// No markdown-structure or gno-form blacklist here: reply bodies\n\t// render inside a \u003cgno-foreign\u003e sandbox (see indentForeignBody),\n\t// which contains block structure and omits forms at render time.\n}\n\nfunc assertMembersUpdateIsEnabled(boardID boards.ID) {\n\tif boardID != 0 {\n\t\tassertRealmIsNotLocked()\n\t} else {\n\t\tassertRealmMembersAreNotLocked()\n\t}\n}\n"},{"name":"public_ban.gno","body":"package boards2\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\n// Constants for different banning periods.\nconst (\n\tBanDay  = uint(24)\n\tBanWeek = BanDay * 7\n\tBanYear = BanDay * 365\n)\n\n// Ban bans a user from a board for a period of time.\n// Only invited guest members and external users can be banned.\n// Banning board owners, admins and moderators is not allowed.\nfunc Ban(cur realm, boardID boards.ID, user address, hours uint, reason string) {\n\tassertAddressIsValid(user)\n\n\tif hours == 0 {\n\t\tpanic(\"ban period in hours is required\")\n\t}\n\n\treason = strings.TrimSpace(reason)\n\tif reason == \"\" {\n\t\tpanic(\"ban reason is required\")\n\t}\n\n\tboard := mustGetBoard(boardID)\n\tcaller := cur.Previous().Address()\n\tuntil := time.Now().Add(time.Minute * 60 * time.Duration(hours))\n\targs := boards.Args{boardID, user, until, reason}\n\tboard.Permissions.WithPermission(caller, PermissionUserBan, args, func() {\n\t\t// When banning invited members make sure they are guests, otherwise\n\t\t// disallow banning. Only guest or external users can be banned.\n\t\tif board.Permissions.HasUser(user) \u0026\u0026 !board.Permissions.HasRole(user, RoleGuest) {\n\t\t\tpanic(\"owner, admin and moderator banning is not allowed\")\n\t\t}\n\n\t\tbanned, found := getBannedUsers(boardID)\n\t\tif !found {\n\t\t\tbanned = bptree.NewBPTree32()\n\t\t\tgBannedUsers.Set(boardID.Key(), banned)\n\t\t}\n\n\t\tbanned.Set(user.String(), until)\n\n\t\tchain.Emit(\n\t\t\t\"UserBanned\",\n\t\t\t\"bannedBy\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"user\", user.String(),\n\t\t\t\"until\", until.Format(time.RFC3339),\n\t\t\t\"reason\", reason,\n\t\t)\n\t})\n}\n\n// Unban unbans a user from a board.\nfunc Unban(cur realm, boardID boards.ID, user address, reason string) {\n\tassertAddressIsValid(user)\n\n\tboard := mustGetBoard(boardID)\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{boardID, user, reason}\n\tboard.Permissions.WithPermission(caller, PermissionUserUnban, args, func() {\n\t\tbanned, found := getBannedUsers(boardID)\n\t\tif !found || !banned.Has(user.String()) {\n\t\t\tpanic(\"user is not banned\")\n\t\t}\n\n\t\tbanned.Remove(user.String())\n\n\t\tchain.Emit(\n\t\t\t\"UserUnbanned\",\n\t\t\t\"bannedBy\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"user\", user.String(),\n\t\t\t\"reason\", reason,\n\t\t)\n\t})\n}\n\n// IsBanned checks if a user is banned from a board.\nfunc IsBanned(boardID boards.ID, user address) bool {\n\tbanned, found := getBannedUsers(boardID)\n\treturn found \u0026\u0026 banned.Has(user.String())\n}\n\nfunc assertAddressIsValid(addr address) {\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address: \" + addr.String())\n\t}\n}\n\nfunc assertUserIsNotBanned(boardID boards.ID, user address) {\n\tbanned, found := getBannedUsers(boardID)\n\tif !found {\n\t\treturn\n\t}\n\n\tv := banned.Get(user.String())\n\tif v == nil {\n\t\treturn\n\t}\n\n\tuntil := v.(time.Time)\n\tif time.Now().Before(until) {\n\t\tpanic(user.String() + \" is banned until \" + until.Format(dateFormat))\n\t}\n}\n"},{"name":"public_flag.gno","body":"package boards2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// SetFlaggingThreshold sets the number of flags required to hide a thread or comment.\n//\n// Threshold is only applicable within the board where it's setted.\nfunc SetFlaggingThreshold(cur realm, boardID boards.ID, threshold int) {\n\tif threshold \u003c 1 {\n\t\tpanic(\"invalid flagging threshold\")\n\t}\n\n\tassertRealmIsNotLocked()\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{board.ID, threshold}\n\tboard.Permissions.WithPermission(caller, PermissionBoardFlaggingUpdate, args, func() {\n\t\tassertRealmIsNotLocked()\n\n\t\tgFlaggingThresholds.Set(boardID.String(), threshold)\n\n\t\tchain.Emit(\n\t\t\t\"FlaggingThresholdUpdated\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threshold\", strconv.Itoa(threshold),\n\t\t)\n\t})\n}\n\n// GetFlaggingThreshold returns the number of flags required to hide a thread or comment within a board.\nfunc GetFlaggingThreshold(boardID boards.ID) int {\n\tassertBoardExists(boardID)\n\treturn getFlaggingThreshold(boardID)\n}\n\n// FlagThread adds a new flag to a thread.\n//\n// Flagging requires special permissions and hides the thread when\n// the number of flags reaches a pre-defined flagging threshold.\nfunc FlagThread(cur realm, boardID, threadID boards.ID, reason string) {\n\treason = strings.TrimSpace(reason)\n\tif reason == \"\" {\n\t\tpanic(\"flagging reason is required\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tboard := mustGetBoard(boardID)\n\tisRealmOwner := gPerms.HasRole(caller, RoleOwner)\n\tif !isRealmOwner {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tthread, found := getThread(board, threadID)\n\tif !found {\n\t\tpanic(\"thread not found\")\n\t}\n\n\tif thread.Hidden {\n\t\tpanic(\"flagging hidden threads is not allowed\")\n\t}\n\n\tflagThread := func() {\n\t\tif thread.Hidden {\n\t\t\tpanic(\"flagged thread is already hidden\")\n\t\t}\n\n\t\t// Hide thread when flagging threshold is reached.\n\t\t// Realm owners can hide with a single flag.\n\t\thide := flagItem(thread, caller, reason, getFlaggingThreshold(board.ID))\n\t\tif hide || isRealmOwner {\n\t\t\t// Remove thread from the list of visible threads\n\t\t\tthread, removed := board.Threads.Remove(threadID)\n\t\t\tif !removed {\n\t\t\t\tpanic(\"thread not found\")\n\t\t\t}\n\n\t\t\t// Mark thread as hidden to avoid rendering content\n\t\t\tthread.Hidden = true\n\n\t\t\t// Keep track of hidden the thread to be able to restore it after moderation disputes\n\t\t\tmeta := board.Meta.(*BoardMeta)\n\t\t\tmeta.HiddenThreads.Add(thread)\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"ThreadFlagged\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"reason\", reason,\n\t\t)\n\t}\n\n\t// Realm owners should be able to flag without permissions even when board is frozen\n\tif isRealmOwner {\n\t\tflagThread()\n\t\treturn\n\t}\n\n\targs := boards.Args{caller, board.ID, thread.ID, reason}\n\tboard.Permissions.WithPermission(caller, PermissionThreadFlag, args, func() {\n\t\tflagThread()\n\t})\n}\n\n// FlagReply adds a new flag to a comment or reply.\n//\n// Flagging requires special permissions and hides the comment or reply\n// when the number of flags reaches a pre-defined flagging threshold.\nfunc FlagReply(cur realm, boardID, threadID, replyID boards.ID, reason string) {\n\treason = strings.TrimSpace(reason)\n\tif reason == \"\" {\n\t\tpanic(\"flagging reason is required\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tboard := mustGetBoard(boardID)\n\tisRealmOwner := gPerms.HasRole(caller, RoleOwner)\n\tif !isRealmOwner {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tthread := mustGetThread(board, threadID)\n\treply := mustGetReply(thread, replyID)\n\tif reply.Hidden {\n\t\tpanic(\"flagging hidden comments or replies is not allowed\")\n\t}\n\n\tflagReply := func() {\n\t\tif reply.Hidden {\n\t\t\tpanic(\"flagged comment or reply is already hidden\")\n\t\t}\n\n\t\thide := flagItem(reply, caller, reason, getFlaggingThreshold(board.ID))\n\t\tif hide || isRealmOwner {\n\t\t\treply.Hidden = true\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"ReplyFlagged\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"replyID\", reply.ID.String(),\n\t\t\t\"reason\", reason,\n\t\t)\n\t}\n\n\t// Realm owners should be able to flag without permissions even when board is frozen\n\tif isRealmOwner {\n\t\tflagReply()\n\t\treturn\n\t}\n\n\targs := boards.Args{caller, board.ID, thread.ID, reply.ID, reason}\n\tboard.Permissions.WithPermission(caller, PermissionReplyFlag, args, func() {\n\t\tflagReply()\n\t})\n}\n"},{"name":"public_freeze.gno","body":"package boards2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// FreezeBoard freezes a board so no more threads and comments can be created or modified.\nfunc FreezeBoard(cur realm, boardID boards.ID) {\n\tsetBoardReadonly(0, cur, boardID, true)\n}\n\n// UnfreezeBoard removes frozen status from a board.\nfunc UnfreezeBoard(cur realm, boardID boards.ID) {\n\tsetBoardReadonly(0, cur, boardID, false)\n}\n\n// IsBoardFrozen checks if a board has been frozen.\nfunc IsBoardFrozen(boardID boards.ID) bool {\n\tboard := mustGetBoard(boardID)\n\treturn board.Readonly\n}\n\n// FreezeThread freezes a thread so thread cannot be replied, modified or deleted.\n//\n// Fails if board is frozen.\nfunc FreezeThread(cur realm, boardID, threadID boards.ID) {\n\tsetThreadReadonly(0, cur, boardID, threadID, true)\n}\n\n// UnfreezeThread removes frozen status from a thread.\n//\n// Fails if board is frozen.\nfunc UnfreezeThread(cur realm, boardID, threadID boards.ID) {\n\tsetThreadReadonly(0, cur, boardID, threadID, false)\n}\n\n// IsThreadFrozen checks if a thread has been frozen.\n//\n// Returns true if board is frozen.\nfunc IsThreadFrozen(boardID, threadID boards.ID) bool {\n\tboard := mustGetBoard(boardID)\n\tthread := mustGetThread(board, threadID)\n\treturn board.Readonly || thread.Readonly\n}\n\nfunc setBoardReadonly(_ int, rlm realm, boardID boards.ID, readonly bool) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tassertRealmIsNotLocked()\n\n\tboard := mustGetBoard(boardID)\n\tif readonly {\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tcaller := rlm.Previous().Address()\n\targs := boards.Args{caller, board.ID, readonly}\n\tboard.Permissions.WithPermission(caller, PermissionBoardFreeze, args, func() {\n\t\tassertRealmIsNotLocked()\n\n\t\tboard.Readonly = readonly\n\n\t\tchain.Emit(\n\t\t\t\"BoardFreeze\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"frozen\", strconv.FormatBool(readonly),\n\t\t)\n\t})\n}\n\nfunc setThreadReadonly(_ int, rlm realm, boardID, threadID boards.ID, readonly bool) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tassertRealmIsNotLocked()\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tthread := mustGetThread(board, threadID)\n\tif readonly {\n\t\tassertThreadIsNotFrozen(thread)\n\t}\n\n\tcaller := rlm.Previous().Address()\n\targs := boards.Args{caller, board.ID, thread.ID, readonly}\n\tboard.Permissions.WithPermission(caller, PermissionThreadFreeze, args, func() {\n\t\tassertRealmIsNotLocked()\n\n\t\tthread.Readonly = readonly\n\n\t\tchain.Emit(\n\t\t\t\"ThreadFreeze\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"threadID\", thread.ID.String(),\n\t\t\t\"frozen\", strconv.FormatBool(readonly),\n\t\t)\n\t})\n}\n"},{"name":"public_invite.gno","body":"package boards2\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\n// Invite contains a user invitation.\ntype Invite struct {\n\t// User is the user to invite.\n\tUser address\n\n\t// Role is the optional role to assign to the user.\n\tRole boards.Role\n}\n\n// InviteMember adds a member to the realm or to a board.\n//\n// A role can optionally be specified to be assigned to the new member.\nfunc InviteMember(cur realm, boardID boards.ID, user address, role boards.Role) {\n\tinviteMembers(0, cur, boardID, Invite{\n\t\tUser: user,\n\t\tRole: role,\n\t})\n}\n\n// InviteMembers adds one or more members to the realm or to a board.\n//\n// Board ID is only required when inviting a member to a specific board.\nfunc InviteMembers(cur realm, boardID boards.ID, invites ...Invite) {\n\tinviteMembers(0, cur, boardID, invites...)\n}\n\n// RequestInvite request to be invited to a board.\nfunc RequestInvite(cur realm, boardID boards.ID) {\n\tassertMembersUpdateIsEnabled(boardID)\n\n\tif !cur.Previous().IsUser() {\n\t\tpanic(\"caller must be user\")\n\t}\n\n\t// TODO: Request a fee (returned on accept) or registered user to avoid spam?\n\t//   WARNING: if a fee is added via unsafe.OriginSend(), the guard above\n\t//   must be tightened to IsUserCall() — IsUser() accepts maketx-run\n\t//   ephemeral realms which can consume the origin-send envelope before\n\t//   calling us, bypassing the fee. See\n\t//   docs/resources/effective-gno.md#verifying-inbound-coin-payments.\n\t// TODO: Make open invite requests optional (per board)\n\n\tboard := mustGetBoard(boardID)\n\tuser := cur.Previous().Address()\n\tif board.Permissions.HasUser(user) {\n\t\tpanic(\"caller is already a member\")\n\t}\n\n\tinvitee := user.String()\n\trequests, found := getInviteRequests(boardID)\n\tif !found {\n\t\trequests = bptree.NewBPTree32()\n\t\trequests.Set(invitee, time.Now())\n\t\tgInviteRequests.Set(boardID.Key(), requests)\n\t\treturn\n\t}\n\n\tif requests.Has(invitee) {\n\t\tpanic(\"invite request already exists\")\n\t}\n\n\trequests.Set(invitee, time.Now())\n}\n\n// AcceptInvite accepts a board invite request.\nfunc AcceptInvite(cur realm, boardID boards.ID, user address) {\n\tassertMembersUpdateIsEnabled(boardID)\n\tassertInviteRequestExists(boardID, user)\n\n\tboard := mustGetBoard(boardID)\n\tif board.Permissions.HasUser(user) {\n\t\tpanic(\"user is already a member\")\n\t}\n\n\tcaller := cur.Previous().Address()\n\tinvite := Invite{\n\t\tUser: user,\n\t\tRole: RoleGuest,\n\t}\n\targs := boards.Args{caller, boardID, []Invite{invite}}\n\tboard.Permissions.WithPermission(caller, PermissionMemberInvite, args, func() {\n\t\tassertMembersUpdateIsEnabled(boardID)\n\n\t\tinvitee := user.String()\n\t\trequests, found := getInviteRequests(boardID)\n\t\tif !found || !requests.Has(invitee) {\n\t\t\tpanic(\"invite request not found\")\n\t\t}\n\n\t\tif board.Permissions.HasUser(user) {\n\t\t\tpanic(\"user is already a member\")\n\t\t}\n\n\t\tboard.Permissions.SetUserRoles(user)\n\t\trequests.Remove(invitee)\n\n\t\tchain.Emit(\n\t\t\t\"MembersInvited\",\n\t\t\t\"invitedBy\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"members\", user.String()+\":\"+string(RoleGuest), // TODO: Support optional role assign\n\t\t)\n\t})\n}\n\n// RevokeInvite revokes a board invite request.\nfunc RevokeInvite(cur realm, boardID boards.ID, user address) {\n\tassertInviteRequestExists(boardID, user)\n\n\tboard := mustGetBoard(boardID)\n\tcaller := cur.Previous().Address()\n\targs := boards.Args{boardID, user, RoleGuest}\n\tboard.Permissions.WithPermission(caller, PermissionMemberInviteRevoke, args, func() {\n\t\tinvitee := user.String()\n\t\trequests, found := getInviteRequests(boardID)\n\t\tif !found || !requests.Has(invitee) {\n\t\t\tpanic(\"invite request not found\")\n\t\t}\n\n\t\trequests.Remove(invitee)\n\n\t\tchain.Emit(\n\t\t\t\"InviteRevoked\",\n\t\t\t\"revokedBy\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"user\", user.String(),\n\t\t)\n\t})\n}\n\nfunc inviteMembers(_ int, rlm realm, boardID boards.ID, invites ...Invite) {\n\tif !rlm.IsCurrent() {\n\t\tpanic(\"unauthorized: rlm is not the caller's live cur\")\n\t}\n\tif len(invites) == 0 {\n\t\tpanic(\"one or more user invites are required\")\n\t}\n\n\tassertMembersUpdateIsEnabled(boardID)\n\tassertNoDuplicatedInvites(invites)\n\n\tperms := mustGetPermissions(boardID)\n\tcaller := rlm.Previous().Address()\n\targs := boards.Args{caller, boardID, invites}\n\tperms.WithPermission(caller, PermissionMemberInvite, args, func() {\n\t\tassertMembersUpdateIsEnabled(boardID)\n\n\t\tusers := make([]string, len(invites))\n\t\tfor _, v := range invites {\n\t\t\tassertMemberAddressIsValid(v.User)\n\n\t\t\tif perms.HasUser(v.User) {\n\t\t\t\tpanic(\"user is already a member: \" + v.User.String())\n\t\t\t}\n\n\t\t\t// NOTE: Permissions implementation should check that role is valid\n\t\t\tperms.SetUserRoles(v.User, v.Role)\n\t\t\tusers = append(users, v.User.String()+\":\"+string(v.Role))\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"MembersInvited\",\n\t\t\t\"invitedBy\", caller.String(),\n\t\t\t\"boardID\", boardID.String(),\n\t\t\t\"members\", strings.Join(users, \",\"),\n\t\t)\n\t})\n}\n\nfunc assertInviteRequestExists(boardID boards.ID, user address) {\n\tinvitee := user.String()\n\trequests, found := getInviteRequests(boardID)\n\tif !found || !requests.Has(invitee) {\n\t\tpanic(\"invite request not found\")\n\t}\n}\n\nfunc assertNoDuplicatedInvites(invites []Invite) {\n\tif len(invites) == 1 {\n\t\treturn\n\t}\n\n\tseen := make(map[address]struct{}, len(invites))\n\tfor _, v := range invites {\n\t\tif _, found := seen[v.User]; found {\n\t\t\tpanic(\"duplicated invite: \" + v.User.String())\n\t\t}\n\n\t\tseen[v.User] = struct{}{}\n\t}\n}\n"},{"name":"public_lock.gno","body":"package boards2\n\nimport (\n\t\"chain\"\n\t\"strconv\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// LockRealm locks the realm making it readonly.\n//\n// WARNING: Realm can't be unlocked once locked.\n//\n// Realm can also be locked without locking realm members.\n// Realm members can be locked when locking the realm or afterwards.\n// This is relevant for two reasons, one so that members can be modified after the lock.\n// The other is for realm owners, who can delete threads and comments after the lock.\nfunc LockRealm(cur realm, lockRealmMembers bool) {\n\tassertRealmMembersAreNotLocked()\n\n\t// If realm members are not being locked assert that realm is not locked.\n\t// Members can be locked after locking the realm, in a second `LockRealm` call.\n\tif !lockRealmMembers {\n\t\tassertRealmIsNotLocked()\n\t}\n\n\tcaller := cur.Previous().Address()\n\tgPerms.WithPermission(caller, PermissionRealmLock, boards.Args{}, func() {\n\t\tgLocked.realm = true\n\t\tgLocked.realmMembers = lockRealmMembers\n\n\t\tchain.Emit(\n\t\t\t\"RealmLocked\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"lockRealmMembers\", strconv.FormatBool(lockRealmMembers),\n\t\t)\n\t})\n}\n\n// IsRealmLocked checks if boards realm has been locked.\nfunc IsRealmLocked() bool {\n\treturn gLocked.realm\n}\n\n// AreRealmMembersLocked checks if realm members have been locked.\nfunc AreRealmMembersLocked() bool {\n\treturn gLocked.realmMembers\n}\n\nfunc assertRealmIsNotLocked() { // TODO: Add filtests for locked realm case to all public functions\n\tif gLocked.realm {\n\t\tpanic(\"realm is locked\")\n\t}\n}\n\nfunc assertRealmMembersAreNotLocked() { // TODO: Add filtests for locked members case to all public member functions\n\tif gLocked.realmMembers {\n\t\tpanic(\"realm and members are locked\")\n\t}\n}\n"},{"name":"render.gno","body":"package boards2\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/markdown/foreign/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n)\n\nconst (\n\tpageSizeDefault = 6\n\tpageSizeReplies = 10\n\t// pageSizeFlat is the page size of the flat \"all comments\" view (?flat=1).\n\t// Each comment renders exactly one \u003cgno-foreign\u003e block (no recursion); with\n\t// the OP (and any repost-source body) the per-page total stays well under\n\t// maxRenderedBodies, so a flat page can't hit the render cap.\n\tpageSizeFlat = 50\n)\n\n// maxRenderedBodies caps user bodies wrapped in \u003cgno-foreign\u003e per page\n// render, kept a margin under gnoweb's per-render foreign-block cap\n// (foreign.MaxBlocksPerRender, sourced from chain/markdown so it can't\n// drift from the renderer). On reaching it the tree stops descending\n// and shows a truncation notice instead of letting the renderer blank\n// comments past the cap. The margin absorbs the OP, repost source\n// bodies, and chrome.\n//\n// Computed lazily (a func, not a package-level var) so it tracks the\n// native cap across chain upgrades: a var initializer runs once at\n// realm-init and persists, freezing a stale value if the underlying\n// MaxBlocksPerRender ever changes; recomputing per render re-reads it.\nfunc maxRenderedBodies() int {\n\treturn foreign.MaxBlocksPerRender() - 10\n}\n\n// sortToggleLink builds the asc/desc sort-toggle link for path. The label\n// names the DESTINATION order (what you get by clicking), not the order you're\n// currently viewing, and ?page= is reset since page N of one order is a\n// different slice in the other. It reports whether the CURRENT order is\n// descending, so callers can drive their own (signed-count or reverse-iterate)\n// pagination. Other query params (e.g. flat=1) are preserved.\nfunc sortToggleLink(path string) (link string, desc bool) {\n\tr := parseRealmPath(path)\n\tdesc = r.Query.Get(\"order\") == \"desc\"\n\tr.Query.Del(\"page\")\n\tif desc {\n\t\tr.Query.Set(\"order\", \"asc\")\n\t\treturn md.Link(\"oldest first\", r.String()), true\n\t}\n\tr.Query.Set(\"order\", \"desc\")\n\treturn md.Link(\"newest first\", r.String()), false\n}\n\nconst menuManageBoard = \"manageBoard\"\n\nvar (\n\tcreateBoardURI = gRealmPath + \":create-board\"\n\tadminUsersURI  = gRealmPath + \":admin-users\"\n\thelpURI        = gRealmPath + \":help\"\n)\n\nfunc Render(path string) string {\n\tvar (\n\t\tb      strings.Builder\n\t\trouter = mux.NewRouter()\n\t)\n\n\trouter.HandleFunc(\"\", renderBoardsList)\n\trouter.HandleFunc(\"help\", renderHelp)\n\trouter.HandleFunc(\"admin-users\", renderMembers)\n\trouter.HandleFunc(\"create-board\", renderCreateBoard)\n\trouter.HandleFunc(\"{board}\", renderBoard)\n\trouter.HandleFunc(\"{board}/members\", renderMembers)\n\trouter.HandleFunc(\"{board}/invites\", renderInvites)\n\trouter.HandleFunc(\"{board}/banned-users\", renderBannedUsers)\n\trouter.HandleFunc(\"{board}/create-thread\", renderCreateThread)\n\trouter.HandleFunc(\"{board}/invite-member\", renderInviteMember)\n\trouter.HandleFunc(\"{board}/{thread}\", renderThread)\n\trouter.HandleFunc(\"{board}/{thread}/flag\", renderFlagPost)\n\trouter.HandleFunc(\"{board}/{thread}/flagging-reasons\", renderFlaggingReasonsPost)\n\trouter.HandleFunc(\"{board}/{thread}/reply\", renderReplyPost)\n\trouter.HandleFunc(\"{board}/{thread}/edit\", renderEditThread)\n\trouter.HandleFunc(\"{board}/{thread}/repost\", renderRepostThread)\n\trouter.HandleFunc(\"{board}/{thread}/{reply}\", renderReply)\n\trouter.HandleFunc(\"{board}/{thread}/{reply}/flag\", renderFlagPost)\n\trouter.HandleFunc(\"{board}/{thread}/{reply}/flagging-reasons\", renderFlaggingReasonsPost)\n\trouter.HandleFunc(\"{board}/{thread}/{reply}/reply\", renderReplyPost)\n\trouter.HandleFunc(\"{board}/{thread}/{reply}/edit\", renderEditReply)\n\n\trouter.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {\n\t\tres.Write(md.Blockquote(\"Path not found\"))\n\t}\n\n\t// Render common realm header before resolving render path\n\tif Notice != \"\" {\n\t\tb.WriteString(infoAlert(\"Notice\", Notice))\n\t}\n\n\t// Render view for current path\n\tb.WriteString(router.Render(path))\n\n\treturn b.String()\n}\n\nfunc renderHelp(res *mux.ResponseWriter, _ *mux.Request) {\n\tres.Write(md.H1(\"Boards Help\"))\n\tif Help != \"\" {\n\t\tres.Write(Help)\n\t\treturn\n\t}\n\n\tlink := RealmLink.Call(\"SetHelp\", \"content\", \"\")\n\tres.Write(md.H3(\"Help content has not been uploaded\"))\n\tres.Write(\"Do you want to \" + md.Link(\"upload boards help\", link) + \"?\")\n}\n\nfunc renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(md.H1(\"Boards\"))\n\trenderBoardListMenu(res, req)\n\tres.Write(md.HorizontalRule())\n\n\tif gListedBoardsByID.Size() == 0 {\n\t\tres.Write(md.H3(\"Currently there are no boards\"))\n\t\tres.Write(\"Be the first to \" + md.Link(\"create a new board\", createBoardURI) + \"!\")\n\t\treturn\n\t}\n\n\tp := newClampedPager(req.RawPath, gListedBoardsByID.Size(), pageSizeDefault)\n\n\trender := func(_ string, v any) bool {\n\t\tboard := v.(*boards.Board)\n\t\tuserLink := userLink(board.Creator)\n\t\tdate := board.CreatedAt.Format(dateFormat)\n\n\t\tres.Write(md.H6(md.Link(board.Name, makeBoardURI(board))))\n\t\tres.Write(\"Created by \" + userLink + \" on \" + date + \", #\" + board.ID.String() + \"  \\n\")\n\n\t\tstatus := strconv.Itoa(board.Threads.Size()) + \" threads\"\n\t\tif board.Readonly {\n\t\t\tstatus += \", read-only\"\n\t\t}\n\n\t\tres.Write(md.Bold(status) + \"\\n\\n\")\n\t\treturn false\n\t}\n\n\tres.Write(\"Sort by: \")\n\tlink, desc := sortToggleLink(req.RawPath)\n\tres.Write(link + \"\\n\\n\")\n\tif desc {\n\t\tgListedBoardsByID.ReverseIterateByOffset(p.Offset(), p.PageSize(), render)\n\t} else {\n\t\tgListedBoardsByID.IterateByOffset(p.Offset(), p.PageSize(), render)\n\t}\n\n\tif p.HasPages() {\n\t\tres.Write(md.HorizontalRule())\n\t\tres.Write(pager.Picker(p))\n\t}\n}\n\nfunc renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(md.Link(\"Create Board\", createBoardURI))\n\tres.Write(\" • \")\n\tres.Write(md.Link(\"List Admin Users\", adminUsersURI))\n\tres.Write(\" • \")\n\tres.Write(md.Link(\"Help\", helpURI))\n\tres.Write(\"\\n\\n\")\n}\n\nfunc renderCreateBoard(res *mux.ResponseWriter, _ *mux.Request) {\n\tform := mdform.New(\"exec\", \"CreateBoard\")\n\tform.Input(\n\t\t\"name\",\n\t\t\"placeholder\", \"Board name\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Radio(\n\t\t\"listed\",\n\t\t\"true\",\n\t\t\"checked\", \"true\",\n\t\t\"description\", \"Should board be publicly listed?\",\n\t)\n\tform.Radio(\n\t\t\"listed\",\n\t\t\"false\",\n\t)\n\tform.Radio(\n\t\t\"open\",\n\t\t\"true\",\n\t\t\"description\", \"Should anyone be allowed to create threads and comments?\",\n\t)\n\tform.Radio(\n\t\t\"open\",\n\t\t\"false\",\n\t\t\"checked\", \"true\",\n\t)\n\n\tres.Write(md.H1(\"Boards: Create Board\"))\n\tres.Write(md.Link(\"← Back to boards\", gRealmPath) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"Boards are by default listed by the realm but they can optionally \" +\n\t\t\t\t\"be created so they are only found by their URL.\",\n\t\t),\n\t)\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"They can also be created to be open so anyone is allowed to create \" +\n\t\t\t\t\"new threads and also to comment on any thread within the open board.\",\n\t\t),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to boards\", gRealmPath) + \"\\n\")\n}\n\nfunc renderMembers(res *mux.ResponseWriter, req *mux.Request) {\n\tboardID := boards.ID(0)\n\tperms := gPerms\n\tname := req.GetVar(\"board\")\n\tif name != \"\" {\n\t\tboard, found := gBoards.GetByName(name)\n\t\tif !found {\n\t\t\tres.Write(md.H3(\"Board not found\"))\n\t\t\treturn\n\t\t}\n\n\t\tboardID = board.ID\n\t\tperms = board.Permissions\n\n\t\tres.Write(md.H1(board.Name + \" Members\"))\n\t\tres.Write(md.H3(\"These are the board members\"))\n\t} else {\n\t\tres.Write(md.H1(\"Admin Users\"))\n\t\tres.Write(md.H3(\"These are the admin users of the realm\"))\n\t}\n\n\t// Create a pager with a small page size to reduce\n\t// the number of username lookups per page.\n\tp := newClampedPager(req.RawPath, perms.UsersCount(), pageSizeDefault)\n\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"Member\", \"Role\", \"Actions\"},\n\t}\n\n\tperms.IterateUsers(p.Offset(), p.PageSize(), func(u boards.User) bool {\n\t\tactions := []string{\n\t\t\tmd.Link(\"remove\", RealmLink.Call(\n\t\t\t\t\"RemoveMember\",\n\t\t\t\t\"boardID\", boardID.String(),\n\t\t\t\t\"member\", u.Address.String(),\n\t\t\t)),\n\t\t\tmd.Link(\"change role\", RealmLink.Call(\n\t\t\t\t\"ChangeMemberRole\",\n\t\t\t\t\"boardID\", boardID.String(),\n\t\t\t\t\"member\", u.Address.String(),\n\t\t\t\t\"role\", \"\",\n\t\t\t)),\n\t\t}\n\n\t\ttable.Append([]string{\n\t\t\tuserLink(u.Address),\n\t\t\trolesToString(u.Roles),\n\t\t\tstrings.Join(actions, \" • \"),\n\t\t})\n\t\treturn false\n\t})\n\tres.Write(table.String())\n\n\tif p.HasPages() {\n\t\tres.Write(\"\\n\" + pager.Picker(p))\n\t}\n}\n\nfunc renderInvites(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(md.H3(\"Board not found\"))\n\t\treturn\n\t}\n\n\tres.Write(md.H1(board.Name + \" Invite Requests\"))\n\n\trequests, found := getInviteRequests(board.ID)\n\tif !found || requests.Size() == 0 {\n\t\tres.Write(md.H3(\"Board has no invite requests\"))\n\t\treturn\n\t}\n\n\tp := newClampedPager(req.RawPath, requests.Size(), pageSizeDefault)\n\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"User\", \"Request Date\", \"Actions\"},\n\t}\n\n\tres.Write(md.H3(\"These users have requested to be invited to the board\"))\n\trequests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {\n\t\tactions := []string{\n\t\t\tmd.Link(\"accept\", RealmLink.Call(\n\t\t\t\t\"AcceptInvite\",\n\t\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\t\"user\", addr,\n\t\t\t)),\n\t\t\tmd.Link(\"revoke\", RealmLink.Call(\n\t\t\t\t\"RevokeInvite\",\n\t\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\t\"user\", addr,\n\t\t\t)),\n\t\t}\n\n\t\ttable.Append([]string{\n\t\t\tuserLink(address(addr)),\n\t\t\tv.(time.Time).Format(dateFormat),\n\t\t\tstrings.Join(actions, \" • \"),\n\t\t})\n\t\treturn false\n\t})\n\n\tres.Write(table.String())\n\n\tif p.HasPages() {\n\t\tres.Write(\"\\n\" + pager.Picker(p))\n\t}\n}\n\nfunc renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(md.H3(\"Board not found\"))\n\t\treturn\n\t}\n\n\tres.Write(md.H1(board.Name + \" Banned Users\"))\n\n\tbanned, found := getBannedUsers(board.ID)\n\tif !found || banned.Size() == 0 {\n\t\tres.Write(md.H3(\"Board has no banned users\"))\n\t\treturn\n\t}\n\n\tp := newClampedPager(req.RawPath, banned.Size(), pageSizeDefault)\n\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"User\", \"Banned Until\", \"Actions\"},\n\t}\n\n\tres.Write(md.H3(\"These users have been banned from the board\"))\n\tbanned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {\n\t\ttable.Append([]string{\n\t\t\tuserLink(address(addr)),\n\t\t\tv.(time.Time).Format(dateFormat),\n\t\t\tmd.Link(\"unban\", RealmLink.Call(\n\t\t\t\t\"Unban\",\n\t\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\t\"user\", addr,\n\t\t\t\t\"reason\", \"\",\n\t\t\t)),\n\t\t})\n\t\treturn false\n\t})\n\n\tres.Write(table.String())\n\n\tif p.HasPages() {\n\t\tres.Write(\"\\n\" + pager.Picker(p))\n\t}\n}\n\nfunc infoAlert(title, msg string) string {\n\theader := strings.TrimSpace(\"[!INFO] \" + title)\n\treturn md.Blockquote(header + \"\\n\" + msg)\n}\n\nfunc rolesToString(roles []boards.Role) string {\n\tif len(roles) == 0 {\n\t\treturn \"\"\n\t}\n\n\tnames := make([]string, len(roles))\n\tfor i, r := range roles {\n\t\tnames[i] = string(r)\n\t}\n\treturn strings.Join(names, \", \")\n}\n\nfunc menuURL(name string) string {\n\t// TODO: Menu URL works because no other GET arguments are being used\n\treturn \"?menu=\" + name\n}\n\nfunc getCurrentMenu(rawURL string) string {\n\t_, rawQuery, found := strings.Cut(rawURL, \"?\")\n\tif !found {\n\t\treturn \"\"\n\t}\n\n\tquery, _ := url.ParseQuery(rawQuery)\n\treturn query.Get(\"menu\")\n}\n"},{"name":"render_board.gno","body":"package boards2\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/mdalert/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n)\n\nfunc renderBoard(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(md.H3(\"The board you are looking for does not exist\"))\n\t\tres.Write(\"Do you want to \" + md.Link(\"create a new board\", createBoardURI) + \"?\")\n\t\treturn\n\t}\n\n\tcreatorLink := userLink(board.Creator)\n\tdate := board.CreatedAt.Format(dateFormat)\n\n\tres.Write(md.H1(md.Link(\"Boards\", gRealmPath) + \" › \" + board.Name))\n\tif board.Readonly {\n\t\tres.Write(\n\t\t\tmdalert.Warning(\"Info\", \"Creating new threads and commenting are disabled within this board\") + \"\\n\",\n\t\t)\n\t}\n\n\tres.Write(\"Created by \" + creatorLink + \" on \" + date + \", #\" + board.ID.String())\n\tres.Write(\"  \\n\" + renderBoardMenu(board, req))\n\tres.Write(md.HorizontalRule())\n\n\tif board.Threads.Size() == 0 {\n\t\tres.Write(md.H3(\"This board doesn't have any threads\"))\n\t\tif !board.Readonly {\n\t\t\tstartConversationLink := md.Link(\"start a new conversation\", makeCreateThreadURI(board))\n\t\t\tres.Write(\"Do you want to \" + startConversationLink + \" in this board?\")\n\t\t}\n\t\treturn\n\t}\n\n\tp := newClampedPager(req.RawPath, board.Threads.Size(), pageSizeDefault)\n\n\trender := func(thread *boards.Post) bool {\n\t\tres.Write(renderThreadSummary(thread) + \"\\n\")\n\t\treturn false\n\t}\n\n\tres.Write(\"Sort by: \")\n\n\tlink, desc := sortToggleLink(req.RawPath)\n\tres.Write(link + \"\\n\\n\")\n\n\tcount := p.PageSize()\n\tif desc {\n\t\tcount = -count // Reverse iterate\n\t}\n\n\tboard.Threads.Iterate(p.Offset(), count, render)\n\n\tif p.HasPages() {\n\t\tres.Write(md.HorizontalRule())\n\t\tres.Write(pager.Picker(p))\n\t}\n}\n\n// renderSubMenu renders a sub-menu with a distinct visual pattern.\nfunc renderSubMenu(items []string) string {\n\tif len(items) == 0 {\n\t\treturn \"\"\n\t}\n\treturn \"└─ \" + strings.Join(items, \" • \") + \"\\n\"\n}\n\nfunc renderBoardMenu(board *boards.Board, req *mux.Request) string {\n\tvar (\n\t\tb               strings.Builder\n\t\tboardMembersURL = makeBoardURI(board) + \"/members\"\n\t)\n\n\tif board.Readonly {\n\t\tb.WriteString(md.Link(\"List Members\", boardMembersURL))\n\t\tb.WriteString(\" • \")\n\t\tb.WriteString(md.Link(\"Unfreeze Board\", makeUnfreezeBoardURI(board)))\n\t\tb.WriteString(\"\\n\")\n\t} else {\n\t\tb.WriteString(\"↳ \")\n\t\tb.WriteString(md.Link(\"Create Thread\", makeCreateThreadURI(board)))\n\t\tb.WriteString(\" • \")\n\t\tb.WriteString(md.Link(\"Request Invite\", makeRequestInviteURI(board)))\n\t\tb.WriteString(\" • \")\n\n\t\tmenu := getCurrentMenu(req.RawPath)\n\t\tif menu == menuManageBoard {\n\t\t\tb.WriteString(md.Bold(\"Manage Board\"))\n\t\t} else {\n\t\t\tb.WriteString(md.Link(\"Manage Board\", menuURL(menuManageBoard)))\n\t\t}\n\n\t\tb.WriteString(\"  \\n\")\n\n\t\tif menu == menuManageBoard {\n\t\t\tsubMenuItems := []string{\n\t\t\t\tmd.Link(\"Invite Member\", makeInviteMemberURI(board)),\n\t\t\t\tmd.Link(\"List Invite Requests\", makeBoardURI(board)+\"/invites\"),\n\t\t\t\tmd.Link(\"List Members\", boardMembersURL),\n\t\t\t\tmd.Link(\"List Banned Users\", makeBoardURI(board)+\"/banned-users\"),\n\t\t\t\tmd.Link(\"Freeze Board\", makeFreezeBoardURI(board)),\n\t\t\t}\n\t\t\tb.WriteString(renderSubMenu(subMenuItems))\n\t\t}\n\t}\n\n\tb.WriteString(\"\\n\")\n\treturn b.String()\n}\n\nfunc renderInviteMember(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"InviteMember\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"user\",\n\t\t\"placeholder\", \"Address\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleOwner),\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleAdmin),\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleModerator),\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleGuest),\n\t\t\"selected\", \"true\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Invite Member\"))\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"Both open and invite only boards can have multiple members with different roles within a \"+\n\t\t\t\t\"board, where members can have a single role at a time.\",\n\t\t) +\n\t\t\tmd.Paragraph(\n\t\t\t\t\"Boards are independent communities which could apply different permissions per role than \"+\n\t\t\t\t\t\"other boards, but generally Boards2 supports four roles, _owner_, _admin_, _moderator_ \"+\n\t\t\t\t\t\"and _guest_.\",\n\t\t\t) +\n\t\t\tmd.Paragraph(\n\t\t\t\t\"Member will be added to \"+md.Link(board.Name, makeBoardURI(board))+\" board.\",\n\t\t\t),\n\t)\n\tres.Write(form.String())\n}\n"},{"name":"render_post.gno","body":"package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/markdown/foreign/v0\"\n\t\"gno.land/p/nt/mdalert/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// renderPost renders a post and (unless it's a leaf or capped) its replies.\n// desc is the page's sort order, forwarded so a comment's nested inline\n// children (renderSubReplies) match the order of the view they appear in. It\n// is unused for the top-level/re-root call (path != \"\" routes to\n// renderTopLevelReplies, which derives the order from the path); those\n// callers pass false.\nfunc renderPost(post *boards.Post, path, indent string, levels int, budget *int, desc bool) string {\n\tvar b strings.Builder\n\n\t// Thread reposts might not have a title, if so get title from source thread\n\ttitle := post.Title\n\tif boards.IsRepost(post) \u0026\u0026 title == \"\" {\n\t\tif board, ok := gBoards.Get(post.OriginalBoardID); ok {\n\t\t\tif src, ok := getThread(board, post.ParentID); ok {\n\t\t\t\ttitle = src.Title\n\t\t\t}\n\t\t}\n\t}\n\n\tif title != \"\" { // Replies don't have a title\n\t\tb.WriteString(md.H2(md.EscapeText(title)))\n\t}\n\n\tb.WriteString(indent + \"\\n\")\n\tb.WriteString(renderPostContent(post, indent, levels, budget))\n\n\tif post.Replies.Size() == 0 {\n\t\treturn b.String()\n\t}\n\n\t// In practice this only fires for the re-rooted view's context-parent,\n\t// which renderPostInner renders with an explicit levels==0. The thread\n\t// recursion does levels-1 in BOTH renderPost and renderTopLevelReplies/\n\t// renderSubReplies, so levels drops by 2 per nesting level and skips 0 —\n\t// i.e. levels does NOT bound depth in the thread view; the render budget\n\t// and the per-node breadth cap (renderSubReplies) are the real bounds.\n\tif levels == 0 {\n\t\tb.WriteString(indent + \"\\n\")\n\t\treturn b.String()\n\t}\n\n\tif path != \"\" {\n\t\tb.WriteString(renderTopLevelReplies(post, path, indent, levels-1, budget))\n\t} else {\n\t\tb.WriteString(renderSubReplies(post, indent, levels-1, budget, desc))\n\t}\n\treturn b.String()\n}\n\nfunc renderPostContent(post *boards.Post, indent string, levels int, budget *int) string {\n\tvar b strings.Builder\n\n\t// Author and date header\n\tcreatorLink := userLink(post.Creator)\n\troleBadge := getRoleBadge(post)\n\tdate := post.CreatedAt.Format(dateFormat)\n\tb.WriteString(indent)\n\tb.WriteString(md.Bold(creatorLink) + roleBadge + \" · \" + date)\n\tif !boards.IsThread(post) {\n\t\tb.WriteString(\" \" + md.Link(\"#\"+post.ID.String(), makeReplyURI(post)))\n\t}\n\tb.WriteString(\"  \\n\")\n\n\t// Flagged comment should be hidden, but replies still visible (see: #3480)\n\t// Flagged threads will be hidden by render function caller.\n\tif post.Hidden {\n\t\tlink := md.Link(\"inappropriate\", makeFlaggingReasonsURI(post))\n\t\tb.WriteString(indentBody(indent, \"⚠ Reply is hidden as it has been flagged as \"+link))\n\t\tb.WriteString(\"\\n\")\n\t\treturn b.String()\n\t}\n\n\tsrcContent, srcPost := renderSourcePost(post, indent, budget)\n\tif boards.IsRepost(post) \u0026\u0026 srcPost != nil {\n\t\tmsg := ufmt.Sprintf(\n\t\t\t\"Original thread is %s  \\nCreated by %s on %s\",\n\t\t\tmd.Link(srcPost.Title, makeThreadURI(srcPost)),\n\t\t\tuserLink(srcPost.Creator),\n\t\t\tsrcPost.CreatedAt.Format(dateFormat),\n\t\t)\n\n\t\tb.WriteString(mdalert.New(mdalert.TypeInfo, \"Thread Repost\", msg, true).String())\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\t// Render repost body before original thread's body\n\tif post.Body != \"\" {\n\t\tb.WriteString(indentForeignBody(indent, post.Body, budget) + \"\\n\")\n\t\tif srcContent != \"\" {\n\t\t\t// Add extra line to separate repost content from original thread content\n\t\t\tb.WriteString(\"\\n\")\n\t\t}\n\t}\n\n\tb.WriteString(srcContent)\n\n\t// Add a newline to separate source deleted message from repost body content\n\tif boards.IsRepost(post) \u0026\u0026 srcPost == nil \u0026\u0026 len(post.Body) \u003e 0 {\n\t\tb.WriteString(\"\\n\\n\")\n\t}\n\n\t// Split thread content and actions\n\tif boards.IsThread(post) \u0026\u0026 !boards.IsRepost(post) {\n\t\tb.WriteString(\"\\n\")\n\t}\n\n\t// Action buttons\n\tb.WriteString(indent)\n\tif !boards.IsThread(post) { // is comment\n\t\tb.WriteString(\"  \\n\")\n\t\tb.WriteString(indent)\n\t}\n\n\tactions := []string{\n\t\tmd.Link(\"Flag\", makeFlagURI(post)),\n\t}\n\n\tif boards.IsThread(post) {\n\t\trepostAction := md.Link(\"Repost\", makeCreateRepostURI(post))\n\t\tif post.Reposts.Size() \u003e 0 {\n\t\t\trepostAction += \" [\" + strconv.Itoa(post.Reposts.Size()) + \"]\"\n\t\t}\n\t\tactions = append(actions, repostAction)\n\t}\n\n\tisReadonly := post.Readonly || post.Board.Readonly\n\t// A reply doesn't carry the thread's frozen flag (FreezeThread sets\n\t// Readonly on the thread post only), so check the enclosing thread too —\n\t// otherwise a frozen thread's replies show Reply/Edit/Delete links that\n\t// the backend rejects. Mirrors the IsReadonly helper (board || thread).\n\tif !isReadonly \u0026\u0026 !boards.IsThread(post) {\n\t\tif t, ok := getThread(post.Board, post.ThreadID); ok {\n\t\t\tisReadonly = t.Readonly\n\t\t}\n\t}\n\tif !isReadonly {\n\t\treplyLabel := \"Reply\"\n\t\tif boards.IsThread(post) {\n\t\t\treplyLabel = \"Comment\"\n\t\t}\n\t\treplyAction := md.Link(replyLabel, makeCreateReplyURI(post))\n\t\t// Add reply count if any\n\t\tif post.Replies.Size() \u003e 0 {\n\t\t\treplyAction += \" [\" + strconv.Itoa(post.Replies.Size()) + \"]\"\n\t\t}\n\n\t\tactions = append(\n\t\t\tactions,\n\t\t\treplyAction,\n\t\t\tmd.Link(\"Edit\", makeEditPostURI(post)),\n\t\t\tmd.Link(\"Delete\", makeDeletePostURI(post)),\n\t\t)\n\t}\n\n\tif levels == 0 {\n\t\tswitch {\n\t\tcase boards.IsThread(post):\n\t\t\tactions = append(actions, md.Link(\"Show all Replies\", makeThreadURI(post)))\n\t\tcase post.Replies.Size() \u003e 0:\n\t\t\t// Reached at levels==0 — in practice the re-rooted view's\n\t\t\t// context-parent (see renderPost). It still has replies below, so\n\t\t\t// re-root here (Reddit/HN \"continue this thread\") to keep the\n\t\t\t// subtree drillable instead of bouncing to the thread root.\n\t\t\tactions = append(actions, md.Link(\"Continue this thread →\", makeReplyURI(post)))\n\t\t}\n\t}\n\n\tb.WriteString(\"↳ \" + strings.Join(actions, \" • \") + \"\\n\")\n\treturn b.String()\n}\n\nfunc renderPostInner(post *boards.Post, path string) string {\n\tif boards.IsThread(post) {\n\t\treturn \"\"\n\t}\n\n\tvar (\n\t\ts         string\n\t\tthreadID  = post.ThreadID\n\t\tthread, _ = getThread(post.Board, threadID)\n\t\tbudget    = maxRenderedBodies()\n\t)\n\n\t// Fully render parent if it's not a repost.\n\tif !boards.IsRepost(post) {\n\t\tparentID := post.ParentID\n\t\tparent := thread\n\n\t\tif thread.ID != parentID {\n\t\t\tparent, _ = getReply(thread, parentID)\n\t\t}\n\n\t\ts += renderPost(parent, \"\", \"\", 0, \u0026budget, false) + \"\\n\"\n\t}\n\n\t// Pass the reply's own path so renderPost routes to renderTopLevelReplies\n\t// and paginates this comment's direct replies (the re-root has its own\n\t// ?page= — no collision with the thread view, which is a different path).\n\t// desc=false: order is derived from path by renderTopLevelReplies.\n\ts += renderPost(post, path, \"\u003e \", 5, \u0026budget, false)\n\treturn s\n}\n\nfunc renderSourcePost(post *boards.Post, indent string, budget *int) (string, *boards.Post) {\n\tif !boards.IsRepost(post) {\n\t\treturn \"\", nil\n\t}\n\n\tindent += \"\u003e \"\n\n\t// TODO: figure out a way to decouple posts from a global storage.\n\tboard, ok := gBoards.Get(post.OriginalBoardID)\n\tif !ok {\n\t\t// TODO: Boards can't be deleted so this might be redundant\n\t\treturn indentBody(indent, \"⚠ Source board has been deleted\"), nil\n\t}\n\n\tsrcPost, ok := getThread(board, post.ParentID)\n\tif !ok {\n\t\treturn indentBody(indent, \"⚠ Source post has been deleted\"), nil\n\t}\n\n\tif srcPost.Hidden {\n\t\treturn indentBody(indent, \"⚠ Source post has been flagged as inappropriate\"), nil\n\t}\n\n\treturn indentForeignBody(indent, srcPost.Body, budget) + \"\\n\\n\", srcPost\n}\n\nfunc renderFlagPost(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\t// Thread ID must always be available\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\t// Parse reply ID when post is a reply\n\tvar reply *boards.Post\n\trawID = req.GetVar(\"reply\")\n\tisReply := rawID != \"\"\n\tif isReply {\n\t\treplyID, err := strconv.Atoi(rawID)\n\t\tif err != nil {\n\t\t\tres.Write(\"Invalid reply ID: \" + md.EscapeText(rawID))\n\t\t\treturn\n\t\t}\n\n\t\treply, _ = getReply(thread, boards.ID(replyID))\n\t\tif reply == nil {\n\t\t\tres.Write(\"Reply not found\")\n\t\t\treturn\n\t\t}\n\t}\n\n\texec := \"FlagThread\"\n\tif isReply {\n\t\texec = \"FlagReply\"\n\t}\n\n\tform := mdform.New(\"exec\", exec)\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\n\tif isReply {\n\t\tform.Input(\n\t\t\t\"replyID\",\n\t\t\t\"placeholder\", \"Reply ID\",\n\t\t\t\"value\", reply.ID.String(),\n\t\t\t\"readonly\", \"true\",\n\t\t)\n\t}\n\n\tform.Input(\n\t\t\"reason\",\n\t\t\"placeholder\", \"Flagging Reason\",\n\t)\n\n\t// Breadcrumb navigation\n\tbackLink := md.Link(\"← Back to thread\", makeThreadURI(thread))\n\n\tif isReply {\n\t\tres.Write(md.H1(board.Name + \": Flag Comment\"))\n\t} else {\n\t\tres.Write(md.H1(board.Name + \": Flag Thread\"))\n\t}\n\tres.Write(backLink + \"\\n\\n\")\n\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"Thread or comment moderation is done through flagging, which is usually done \"+\n\t\t\t\t\"by board members with the moderator role, though other roles could also potentially flag.\",\n\t\t) +\n\t\t\tmd.Paragraph(\n\t\t\t\t\"Flagging relies on a configurable threshold, which by default is of one flag, that when \"+\n\t\t\t\t\t\"reached leads to the flagged thread or comment to be hidden.\",\n\t\t\t) +\n\t\t\tmd.Paragraph(\n\t\t\t\t\"Flagging thresholds can be different within each board.\",\n\t\t\t),\n\t)\n\n\tif isReply {\n\t\tres.Write(\n\t\t\tmd.Paragraph(\n\t\t\t\tufmt.Sprintf(\n\t\t\t\t\t\"⚠ You are flagging a %s from %s ⚠\",\n\t\t\t\t\tmd.Link(\"comment\", makeReplyURI(reply)),\n\t\t\t\t\tuserLink(reply.Creator),\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t} else {\n\t\tres.Write(\n\t\t\tmd.Paragraph(\n\t\t\t\tufmt.Sprintf(\n\t\t\t\t\t\"⚠ You are flagging the thread: %s ⚠\",\n\t\t\t\t\tmd.Link(thread.Title, makeThreadURI(thread)),\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t}\n\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n\nfunc renderFlaggingReasonsPost(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\t// Thread ID must always be available\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\tflags := thread.Flags\n\n\t// Parse reply ID when post is a reply\n\tvar reply *boards.Post\n\trawID = req.GetVar(\"reply\")\n\tisReply := rawID != \"\"\n\tif isReply {\n\t\treplyID, err := strconv.Atoi(rawID)\n\t\tif err != nil {\n\t\t\tres.Write(\"Invalid reply ID: \" + md.EscapeText(rawID))\n\t\t\treturn\n\t\t}\n\n\t\treply, found = getReply(thread, boards.ID(replyID))\n\t\tif !found {\n\t\t\tres.Write(\"Reply not found\")\n\t\t\treturn\n\t\t}\n\n\t\tflags = reply.Flags\n\t}\n\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"Moderator\", \"Reason\"},\n\t}\n\n\tflags.Iterate(0, flags.Size(), func(f boards.Flag) bool {\n\t\t// f.Reason is user-supplied (only trimmed at write); escape it so a\n\t\t// flag reason can't inject markdown (links/images) or HTML into the\n\t\t// reasons table. md.EscapeText leaves '|' for mdtable to escape.\n\t\ttable.Append([]string{userLink(f.User), md.EscapeText(f.Reason)})\n\t\treturn false\n\t})\n\n\t// Breadcrumb navigation\n\tbackLink := md.Link(\"← Back to thread\", makeThreadURI(thread))\n\n\tres.Write(md.H1(\"Flagging Reasons\"))\n\tres.Write(backLink + \"\\n\\n\")\n\tif isReply {\n\t\tres.Write(\n\t\t\tmd.Paragraph(\n\t\t\t\tufmt.Sprintf(\n\t\t\t\t\t\"Moderation flags for a %s submitted by %s\",\n\t\t\t\t\tmd.Link(\"comment\", makeReplyURI(reply)),\n\t\t\t\t\tuserLink(reply.Creator),\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t} else {\n\t\tres.Write(\n\t\t\tmd.Paragraph(\n\t\t\t\t// Intentionally hide flagged thread title\n\t\t\t\tufmt.Sprintf(\"Moderation flags for %s\", md.Link(\"thread\", makeThreadURI(thread))),\n\t\t\t),\n\t\t)\n\t}\n\tres.Write(table.String())\n}\n\nfunc renderReplyPost(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\t// Thread ID must always be available\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := board.Threads.Get(boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\t// Parse reply ID when post is a reply\n\tvar reply *boards.Post\n\trawID = req.GetVar(\"reply\")\n\tisReply := rawID != \"\"\n\tif isReply {\n\t\treplyID, err := strconv.Atoi(rawID)\n\t\tif err != nil {\n\t\t\tres.Write(\"Invalid reply ID: \" + md.EscapeText(rawID))\n\t\t\treturn\n\t\t}\n\n\t\treply, _ = getReply(thread, boards.ID(replyID))\n\t\tif reply == nil {\n\t\t\tres.Write(\"Reply not found\")\n\t\t\treturn\n\t\t}\n\t}\n\n\tform := mdform.New(\"exec\", \"CreateReply\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\n\tif isReply {\n\t\tform.Input(\n\t\t\t\"replyID\",\n\t\t\t\"placeholder\", \"Reply ID\",\n\t\t\t\"value\", reply.ID.String(),\n\t\t\t\"readonly\", \"true\",\n\t\t)\n\t} else {\n\t\tform.Input(\n\t\t\t\"replyID\",\n\t\t\t\"placeholder\", \"Reply ID\",\n\t\t\t\"value\", \"0\",\n\t\t\t\"readonly\", \"true\",\n\t\t)\n\t}\n\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Comment\",\n\t\t\"required\", \"true\",\n\t)\n\n\t// Breadcrumb navigation\n\tbackLink := md.Link(\"← Back to thread\", makeThreadURI(thread))\n\n\tif isReply {\n\t\tres.Write(md.H1(board.Name + \": Reply\"))\n\t\tres.Write(backLink + \"\\n\\n\")\n\t\tres.Write(\n\t\t\tmd.Paragraph(ufmt.Sprintf(\"Replying to a comment posted by %s:\", userLink(reply.Creator))) +\n\t\t\t\tforeign.ForeignWithLabel(\"Quoted comment\", reply.Body),\n\t\t)\n\t} else {\n\t\tres.Write(md.H1(board.Name + \": Comment\"))\n\t\tres.Write(backLink + \"\\n\\n\")\n\t\tres.Write(\n\t\t\tmd.Paragraph(\n\t\t\t\tufmt.Sprintf(\"Commenting on the thread: %s\", md.Link(thread.Title, makeThreadURI(thread))),\n\t\t\t),\n\t\t)\n\t}\n\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n"},{"name":"render_reply.gno","body":"package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc renderReply(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\trawID = req.GetVar(\"reply\")\n\treplyID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid reply ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\treply, found := getReply(thread, boards.ID(replyID))\n\tif !found {\n\t\tres.Write(\"Reply not found\")\n\t\treturn\n\t}\n\n\t// Call render even for hidden replies to display children.\n\t// Original comment content will be hidden under the hood.\n\t// See: #3480\n\tres.Write(renderPostInner(reply, req.RawPath))\n}\n\n// newClampedPager builds a pager, clamping an out-of-range, zero, negative,\n// or malformed ?page= to the last valid page instead of erroring. pager.New\n// rejects page==0, page\u003epageCount, and non-numeric pages with\n// ErrInvalidPageNumber, which the callers would otherwise surface as a panic\n// (aborting the whole render) or an error page in place of the list. A\n// NEGATIVE page is not rejected by pager.New (it only checks ==0 and\n// \u003epageCount), so it must be caught here too — otherwise it renders a broken\n// \"page -N of M\" picker with both arrows disabled. Triggers on a stale deep\n// link after deletions, or a hand-edited URL.\nfunc newClampedPager(path string, size, pageSize int) pager.Pager {\n\tp, err := pager.New(path, size, pager.WithPageSize(pageSize))\n\tif err == nil \u0026\u0026 p.Page() \u003e= 1 {\n\t\treturn p\n\t}\n\t// Re-parse without ?page= to get a valid page-1 pager and read the real\n\t// page count, then jump to the last page when there is more than one.\n\tr := parseRealmPath(path)\n\tr.Query.Del(\"page\")\n\tfirst, ferr := pager.New(r.String(), size, pager.WithPageSize(pageSize))\n\tif ferr != nil {\n\t\t// A page-less path is valid for every current route, so this is\n\t\t// unreachable today; fall back to the original pager rather than\n\t\t// asserting the invariant with a panic that would abort the render.\n\t\treturn p\n\t}\n\tif last := first.PageCount(); last \u003e 1 {\n\t\tr.Query.Set(\"page\", strconv.Itoa(last))\n\t\tif clamped, e := pager.New(r.String(), size, pager.WithPageSize(pageSize)); e == nil {\n\t\t\treturn clamped\n\t\t}\n\t}\n\treturn first\n}\n\nfunc renderTopLevelReplies(post *boards.Post, path, indent string, levels int, budget *int) string {\n\tp := newClampedPager(path, post.Replies.Size(), pageSizeReplies)\n\tlink, desc := sortToggleLink(path)\n\n\tvar (\n\t\tb              strings.Builder\n\t\tcommentsIndent = indent + \"\u003e \"\n\t\ttruncated      bool\n\t)\n\n\trender := func(reply *boards.Post) bool {\n\t\tif *budget \u003c= 0 {\n\t\t\ttruncated = true\n\t\t\treturn true // stop: render budget exhausted (see maxRenderedBodies)\n\t\t}\n\t\t// Forward the page order so this reply's nested children match it.\n\t\tb.WriteString(indent + \"\\n\" + renderPost(reply, \"\", commentsIndent, levels-1, budget, desc))\n\t\treturn false\n\t}\n\n\tb.WriteString(\"\\n\" + md.HorizontalRule() + \"Sort by: \" + link + \"\\n\")\n\n\tcount := p.PageSize()\n\tif desc {\n\t\tcount = -count // Reverse iterate\n\t}\n\n\tpost.Replies.Iterate(p.Offset(), count, render)\n\n\tif truncated {\n\t\tb.WriteString(indent + \"\\n\" + commentsIndent + \"_Some replies not shown — \" +\n\t\t\tmd.Link(\"view all comments\", makeThreadFlatURI(post)) + \"._\\n\")\n\t}\n\n\t// Suppress the page picker when the budget truncated this page: later\n\t// replies in the page were skipped, so the offset-based \"next page\" would\n\t// jump past them. The flat link above is the complete, reachable view.\n\tif !truncated \u0026\u0026 p.HasPages() {\n\t\tb.WriteString(md.HorizontalRule())\n\t\tb.WriteString(pager.Picker(p))\n\t}\n\treturn b.String()\n}\n\nfunc renderSubReplies(post *boards.Post, indent string, levels int, budget *int, desc bool) string {\n\tvar (\n\t\tb              strings.Builder\n\t\tcommentsIndent = indent + \"\u003e \"\n\t\ttruncated      bool\n\t)\n\n\t// Cap inline children at pageSizeReplies. A nested reply list can't have\n\t// its own pager (it would collide with the page's ?page=), so instead of\n\t// dumping every child here a comment with more links to its own re-rooted\n\t// view, which paginates them. Keeps every view bounded to \u003c=pageSizeReplies\n\t// children per post. count's sign follows the page order so the inline\n\t// children match the view they appear in (desc → the newest ones).\n\tcount := pageSizeReplies\n\tif desc {\n\t\tcount = -count\n\t}\n\tpost.Replies.Iterate(0, count, func(reply *boards.Post) bool {\n\t\tif *budget \u003c= 0 {\n\t\t\ttruncated = true\n\t\t\treturn true // stop: render budget exhausted (see maxRenderedBodies)\n\t\t}\n\t\tb.WriteString(indent + \"\\n\" + renderPost(reply, \"\", commentsIndent, levels-1, budget, desc))\n\t\treturn false\n\t})\n\n\tnotice := func(text, uri string) {\n\t\tb.WriteString(indent + \"\\n\" + commentsIndent + md.Link(text, uri) + \"\\n\")\n\t}\n\tswitch {\n\tcase truncated:\n\t\t// Budget exhausted: the whole-thread flat view is the reachable backstop.\n\t\tnotice(\"More replies — view all comments\", makeThreadFlatURI(post))\n\tcase post.Replies.Size() \u003e pageSizeReplies:\n\t\t// Breadth cap hit: re-root at this comment to page the rest, carrying\n\t\t// the page's sort order so the re-root opens the same way (its first\n\t\t// page then matches the newest-first children shown inline here).\n\t\turi := makeReplyURI(post)\n\t\tif desc {\n\t\t\turi += \"?order=desc\"\n\t\t}\n\t\tnotice(\"View all \"+strconv.Itoa(post.Replies.Size())+\" replies\", uri)\n\t}\n\treturn b.String()\n}\n\nfunc renderEditReply(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\trawID = req.GetVar(\"reply\")\n\treplyID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid reply ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\treply, found := getReply(thread, boards.ID(replyID))\n\tif !found {\n\t\tres.Write(\"Reply not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"EditReply\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"replyID\",\n\t\t\"placeholder\", \"Reply ID\",\n\t\t\"value\", reply.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Comment\",\n\t\t\"value\", reply.Body,\n\t\t\"required\", \"true\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Edit Comment\"))\n\tres.Write(md.Link(\"← Back to thread\", makeThreadURI(thread)) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\tufmt.Sprintf(\"Editing a comment from the thread: %s\", md.Link(thread.Title, makeThreadURI(thread))),\n\t\t),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n"},{"name":"render_thread.gno","body":"package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// maxFlatIndentDepth caps the blockquote nesting in the flat comment view so\n// deep reply chains stay readable; comments deeper than this still render,\n// just at the capped indent.\nconst maxFlatIndentDepth = 6\n\nfunc renderThread(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\tif thread.Hidden {\n\t\tlink := md.Link(\"inappropriate\", makeFlaggingReasonsURI(thread))\n\t\tres.Write(\"⚠ Thread has been flagged as \" + link)\n\t\treturn\n\t}\n\n\tres.Write(md.H1(md.Link(\"Boards\", gRealmPath) + \" › \" + md.Link(board.Name, makeBoardURI(board))))\n\tbudget := maxRenderedBodies()\n\tif parseRealmPath(req.RawPath).Query.Get(\"flat\") != \"\" {\n\t\tres.Write(renderThreadFlat(thread, req.RawPath, \u0026budget))\n\t\treturn\n\t}\n\tres.Write(renderPost(thread, req.RawPath, \"\", 5, \u0026budget, false))\n}\n\n// renderThreadFlat renders every comment in the thread as a single flat,\n// depth-indented, paginated list backed by ThreadMeta.AllReplies (the index\n// that already holds every reply at every depth). Unlike the recursive\n// threaded view — which bounds work with the render budget and truncates a\n// large subtree — this view is reachable to the very last comment: each\n// comment renders one \u003cgno-foreign\u003e block (no recursion), so a fixed page\n// size (pageSizeFlat) plus the OP stays well under the budget regardless of\n// nesting, and the pager always advances. ?order=desc shows newest first, so\n// its page 1 is the latest comments.\nfunc renderThreadFlat(thread *boards.Post, path string, budget *int) string {\n\tvar b strings.Builder\n\n\t// The OP for context (its body only; no replies — levels 0).\n\tb.WriteString(renderPost(thread, \"\", \"\", 0, budget, false))\n\n\tmeta, ok := thread.Meta.(*ThreadMeta)\n\tif !ok || meta.AllReplies.Size() == 0 {\n\t\treturn b.String()\n\t}\n\tall := meta.AllReplies\n\tp := newClampedPager(path, all.Size(), pageSizeFlat)\n\n\tb.WriteString(\"\\n\" + md.HorizontalRule())\n\tb.WriteString(md.Link(\"← Threaded view\", makeThreadURI(thread)) + \" · All \" +\n\t\tstrconv.Itoa(all.Size()) + \" comments — sort by: \")\n\n\t// sortToggleLink preserves flat=1, so the toggle stays in the flat view.\n\tlink, desc := sortToggleLink(path)\n\tb.WriteString(link + \"\\n\")\n\n\tcount := p.PageSize()\n\tif desc {\n\t\tcount = -count // reverse iterate: newest first\n\t}\n\tall.Iterate(p.Offset(), count, func(reply *boards.Post) bool {\n\t\tif *budget \u003c= 0 {\n\t\t\t// Unreachable while pageSizeFlat \u003c\u003c maxRenderedBodies; a backstop\n\t\t\t// in case a chain upgrade drops the native cap below one page.\n\t\t\treturn true\n\t\t}\n\t\tindent := flatIndent(thread, reply)\n\t\tb.WriteString(indent + \"\\n\" + renderPost(reply, \"\", indent, 0, budget, false))\n\t\treturn false\n\t})\n\n\tif p.HasPages() {\n\t\tb.WriteString(md.HorizontalRule())\n\t\tb.WriteString(pager.Picker(p))\n\t}\n\treturn b.String()\n}\n\n// flatIndent returns the blockquote indent for a reply in the flat view,\n// derived from its depth below the thread root (depth 1 = a direct reply to\n// the thread), capped at maxFlatIndentDepth.\nfunc flatIndent(thread *boards.Post, reply *boards.Post) string {\n\tdepth := 1\n\tpid := reply.ParentID\n\tfor pid != thread.ID \u0026\u0026 pid != 0 \u0026\u0026 depth \u003c maxFlatIndentDepth {\n\t\tparent, ok := getReply(thread, pid)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tdepth++\n\t\tpid = parent.ParentID\n\t}\n\treturn strings.Repeat(\"\u003e \", depth)\n}\n\nfunc renderThreadSummary(thread *boards.Post) string {\n\tvar (\n\t\tb           strings.Builder\n\t\tpostURI     = makeThreadURI(thread)\n\t\tsummary     = summaryOf(thread.Title, 80)\n\t\tcreatorLink = userLink(thread.Creator)\n\t\troleBadge   = getRoleBadge(thread)\n\t\tdate        = thread.CreatedAt.Format(dateFormat)\n\t)\n\n\tbyline := \"Created by \"\n\tif boards.IsRepost(thread) {\n\t\tsummary += ` ⟳`\n\t\tbyline = \"Reposted by \"\n\t}\n\n\tb.WriteString(md.H6(md.Link(summary, postURI)))\n\tb.WriteString(byline + creatorLink + roleBadge + \" on \" + date + \"  \\n\")\n\n\tstatus := []string{\n\t\tstrconv.Itoa(thread.Replies.Size()) + \" replies\",\n\t\tstrconv.Itoa(thread.Reposts.Size()) + \" reposts\",\n\t}\n\tb.WriteString(md.Bold(strings.Join(status, \" • \")) + \"\\n\")\n\treturn b.String()\n}\n\nfunc renderCreateThread(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"CreateThread\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"title\",\n\t\t\"placeholder\", \"Title\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Content\",\n\t\t\"rows\", \"10\",\n\t\t\"required\", \"true\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Create Thread\"))\n\tres.Write(md.Link(\"← Back to board\", makeBoardURI(board)) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\tufmt.Sprintf(\"Thread will be created in the board: %s\", md.Link(board.Name, makeBoardURI(board))),\n\t\t),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to board\", makeBoardURI(board)) + \"\\n\")\n}\n\nfunc renderEditThread(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"EditThread\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"title\",\n\t\t\"placeholder\", \"Title\",\n\t\t\"value\", thread.Title,\n\t\t\"required\", \"true\",\n\t)\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Content\",\n\t\t\"rows\", \"10\",\n\t\t\"value\", thread.Body,\n\t\t\"required\", \"true\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Edit Thread\"))\n\tres.Write(md.Link(\"← Back to thread\", makeThreadURI(thread)) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\"Editing \" + md.Link(thread.Title, makeThreadURI(thread))),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n\nfunc renderRepostThread(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + md.EscapeText(rawID))\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"CreateRepost\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"destinationBoardID\",\n\t\t\"type\", mdform.InputTypeNumber,\n\t\t\"placeholder\", \"Board ID where to repost\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Input(\n\t\t\"title\",\n\t\t\"value\", thread.Title,\n\t\t\"placeholder\", \"Title\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Content\",\n\t\t\"rows\", \"10\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Repost Thread\"))\n\tres.Write(md.Link(\"← Back to thread\", makeThreadURI(thread)) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"Threads can be reposted to other open boards or boards where you are a member \" +\n\t\t\t\t\"and are allowed to create new threads.\",\n\t\t),\n\t)\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\tufmt.Sprintf(\"Reposting the thread: %s.\", md.Link(thread.Title, makeThreadURI(thread))),\n\t\t),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n"},{"name":"uris_board.gno","body":"package boards2\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\nfunc makeBoardURI(b *boards.Board) string {\n\tpath := strings.TrimPrefix(string(RealmLink), \"gno.land\")\n\treturn path + \":\" + url.PathEscape(b.Name)\n}\n\nfunc makeFreezeBoardURI(b *boards.Board) string {\n\treturn RealmLink.Call(\n\t\t\"FreezeBoard\",\n\t\t\"boardID\", b.ID.String(),\n\t)\n}\n\nfunc makeUnfreezeBoardURI(b *boards.Board) string {\n\treturn RealmLink.Call(\n\t\t\"UnfreezeBoard\",\n\t\t\"boardID\", b.ID.String(),\n\t\t\"threadID\", \"\",\n\t\t\"replyID\", \"\",\n\t)\n}\n\nfunc makeInviteMemberURI(b *boards.Board) string {\n\treturn makeBoardURI(b) + \"/invite-member\"\n}\n\nfunc makeCreateThreadURI(b *boards.Board) string {\n\treturn makeBoardURI(b) + \"/create-thread\"\n}\n\nfunc makeRequestInviteURI(b *boards.Board) string {\n\treturn RealmLink.Call(\n\t\t\"RequestInvite\",\n\t\t\"boardID\", b.ID.String(),\n\t)\n}\n"},{"name":"uris_post.gno","body":"package boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\nfunc makeThreadURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn makeBoardURI(p.Board) + \"/\" + p.ID.String()\n\t}\n\n\t// When post is a reply use the parent thread ID\n\treturn makeBoardURI(p.Board) + \"/\" + p.ThreadID.String()\n}\n\n// makeThreadFlatURI links to the thread's flat \"all comments\" view. Works for\n// a thread or any reply within it (makeThreadURI resolves to the thread).\nfunc makeThreadFlatURI(p *boards.Post) string {\n\treturn makeThreadURI(p) + \"?flat=1\"\n}\n\nfunc makeReplyURI(p *boards.Post) string {\n\treturn makeBoardURI(p.Board) + \"/\" + p.ThreadID.String() + \"/\" + p.ID.String()\n}\n\nfunc makeCreateReplyURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn makeThreadURI(p) + \"/reply\"\n\t}\n\treturn makeReplyURI(p) + \"/reply\"\n}\n\nfunc makeCreateRepostURI(p *boards.Post) string {\n\treturn makeThreadURI(p) + \"/repost\"\n}\n\nfunc makeDeletePostURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn RealmLink.Call(\n\t\t\t\"DeleteThread\",\n\t\t\t\"boardID\", p.Board.ID.String(),\n\t\t\t\"threadID\", p.ThreadID.String(),\n\t\t)\n\t}\n\treturn RealmLink.Call(\n\t\t\"DeleteReply\",\n\t\t\"boardID\", p.Board.ID.String(),\n\t\t\"threadID\", p.ThreadID.String(),\n\t\t\"replyID\", p.ID.String(),\n\t)\n}\n\nfunc makeEditPostURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn makeThreadURI(p) + \"/edit\"\n\t}\n\treturn makeReplyURI(p) + \"/edit\"\n}\n\nfunc makeFlagURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn makeThreadURI(p) + \"/flag\"\n\t}\n\treturn makeReplyURI(p) + \"/flag\"\n}\n\nfunc makeFlaggingReasonsURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn makeThreadURI(p) + \"/flagging-reasons\"\n\t}\n\treturn makeReplyURI(p) + \"/flagging-reasons\"\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"crossrealm_f","path":"gno.land/r/tests/vm/crossrealm_f","files":[{"name":"crossrealm.gno","body":"// Package crossrealm_f provides a collection realm for testing cross-realm\n// ownership scenarios. It uses a nested structure so that intermediate objects\n// in the ownership chain have RefCount == 1.\npackage crossrealm_f\n\ntype Entry struct {\n\tKey   string\n\tValue int\n}\n\nvar entries []*Entry\n\nfunc NewEntry(key string, value int) *Entry {\n\treturn \u0026Entry{Key: key, Value: value}\n}\n\nfunc Add(cur realm, e *Entry) {\n\tentries = append(entries, e)\n}\n\nfunc Remove(cur realm, key string) *Entry {\n\tfor i, e := range entries {\n\t\tif e.Key == key {\n\t\t\tentries = append(entries[:i], entries[i+1:]...)\n\t\t\treturn e\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc Get(key string) *Entry {\n\tfor _, e := range entries {\n\t\tif e.Key == key {\n\t\t\treturn e\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc Len() int {\n\treturn len(entries)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/crossrealm_f\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tests_foo","path":"gno.land/r/tests/vm/tests_foo","files":[{"name":"foo.gno","body":"package tests_foo\n\nimport (\n\ttests \"gno.land/r/tests/vm\"\n)\n\n// for testing gno.land/r/tests/vm/interfaces.go\n\ntype FooStringer struct {\n\tFieldA string\n}\n\nfunc (fs *FooStringer) String() string {\n\treturn \"\u0026FooStringer{\" + fs.FieldA + \"}\"\n}\n\n// AddFooStringer is a non-crossing helper. Callers thread their own\n// live cur as `rlm`; `cross(rlm)` forwards it into the (cur realm)-\n// crossing tests.AddStringer.\nfunc AddFooStringer(_ int, rlm realm, fa string) {\n\ttests.AddStringer(cross(rlm), \u0026FooStringer{fa})\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/tests_foo\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"subtests","path":"gno.land/p/demo/tests/subtests","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/subtests\"\ngno = \"0.9\"\n"},{"name":"subtests.gno","body":"package subtests\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n)\n\nfunc GetCurrentRealm() runtime.Realm {\n\treturn unsafe.CurrentRealm()\n}\n\nfunc GetPreviousRealm() runtime.Realm {\n\treturn unsafe.PreviousRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"blog","path":"gno.land/r/gnoland/blog","files":[{"name":"admin.gno","body":"package blog\n\nimport (\n\t\"chain/runtime/unsafe\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\nvar (\n\terrNotAdmin     = errors.New(\"access restricted: not admin\")\n\terrNotModerator = errors.New(\"access restricted: not moderator\")\n\terrNotCommenter = errors.New(\"access restricted: not commenter\")\n)\n\nvar (\n\tadminAddr     = address(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\") // govdao t1 multisig\n\tmoderatorList = bptree.NewBPTree32()\n\tcommenterList = bptree.NewBPTree32()\n\tinPause       bool\n)\n\nfunc AdminSetAdminAddr(_ realm, addr address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(_ realm, state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(_ realm, addr address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(_ realm, addr address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // entry kept as a revocation record; isModerator checks the value\n}\n\nfunc NewPostProposalRequest(cur realm, slug, title, body, publicationDate, authors, tags string) dao.ProposalRequest {\n\tcaller := cur.Previous().Address()\n\te := dao.NewSimpleExecutor(0, cur,\n\t\tfunc(realm) error {\n\t\t\taddPost(caller, slug, title, body, publicationDate, authors, tags)\n\n\t\t\treturn nil\n\t\t},\n\t\tufmt.Sprintf(\"- Post Title: %v\\n- Post Publication Date: %v\\n- Authors: %v\\n- Tags: %v\", title, publicationDate, authors, tags),\n\t)\n\n\treturn dao.NewProposalRequest(\n\t\t\"Add new post to gnoland blog\",\n\t\t\"This propoposal is looking to add a new post to gnoland blog\",\n\t\te,\n\t)\n}\n\nfunc ModAddPost(_ realm, slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\tcaller := unsafe.OriginCaller()\n\taddPost(caller, slug, title, body, publicationDate, authors, tags)\n}\n\nfunc addPost(caller address, slug, title, body, publicationDate, authors, tags string) {\n\tvar tagList []string\n\tif tags != \"\" {\n\t\ttagList = strings.Split(tags, \",\")\n\t}\n\tvar authorList []string\n\tif authors != \"\" {\n\t\tauthorList = strings.Split(authors, \",\")\n\t}\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\n\tcheckErr(err)\n}\n\nfunc ModEditPost(_ realm, slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModRemovePost(_ realm, slug string) {\n\tassertIsModerator()\n\tb.RemovePost(slug)\n}\n\nfunc ModAddCommenter(_ realm, addr address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), true)\n}\n\nfunc ModDelCommenter(_ realm, addr address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), false) // entry kept as a revocation record; isCommenter checks the value\n}\n\nfunc ModDelComment(_ realm, slug string, index int) {\n\tassertIsModerator()\n\terr := b.GetPost(slug).DeleteComment(index)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr address) bool {\n\t// Removed moderators stay in the list with a false value, so the\n\t// stored value must be checked, not just key presence.\n\tactive, _ := moderatorList.Get(addr.String()).(bool)\n\treturn active\n}\n\nfunc isCommenter(addr address) bool {\n\t// Removed commenters stay in the list with a false value, so the\n\t// stored value must be checked, not just key presence.\n\tactive, _ := commenterList.Get(addr.String()).(bool)\n\treturn active\n}\n\nfunc assertIsAdmin() {\n\tcaller := unsafe.OriginCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(errNotAdmin.Error())\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := unsafe.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(errNotModerator.Error())\n}\n\nfunc assertIsCommenter() {\n\tcaller := unsafe.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) || isCommenter(caller) {\n\t\treturn\n\t}\n\tpanic(errNotCommenter.Error())\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n"},{"name":"gnoblog.gno","body":"package blog\n\nimport (\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/demo/blog\"\n)\n\nvar b = \u0026blog.Blog{\n\tTitle:  \"Gno.land's blog\",\n\tPrefix: \"/r/gnoland/blog:\",\n}\n\nfunc AddComment(_ realm, postSlug, comment string) {\n\tassertIsCommenter()\n\tassertNotInPause()\n\n\tcaller := unsafe.OriginCaller()\n\terr := b.GetPost(postSlug).AddComment(caller, comment)\n\tcheckErr(err)\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n\nfunc RenderLastPostsWidget(limit int) string {\n\treturn b.RenderLastPostsWidget(limit)\n}\n\nfunc PostExists(slug string) bool {\n\tif b.GetPost(slug) == nil {\n\t\treturn false\n\t}\n\treturn true\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/blog\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"util.gno","body":"package blog\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"dynreplacer","path":"gno.land/p/moul/dynreplacer","files":[{"name":"dynreplacer.gno","body":"// Package dynreplacer provides a simple template engine for handling dynamic\n// content replacement. It is similar to strings.Replacer but with lazy\n// execution of replacements, making it more optimization-friendly in several\n// cases. While strings.Replacer requires all replacement values to be computed\n// upfront, dynreplacer only executes the callback functions for placeholders\n// that actually exist in the template, avoiding unnecessary computations.\n//\n// The package ensures efficient, non-recursive replacement of placeholders in a\n// single pass. This lazy evaluation approach is particularly beneficial when:\n// - Some replacement values are expensive to compute\n// - Not all placeholders are guaranteed to be present in the template\n// - Templates are reused with different content\n//\n// Example usage:\n//\n//\tr := dynreplacer.New(\n//\t    dynreplacer.Pair{\":name:\", func() string { return \"World\" }},\n//\t    dynreplacer.Pair{\":greeting:\", func() string { return \"Hello\" }},\n//\t)\n//\tresult := r.Replace(\"Hello :name:!\") // Returns \"Hello World!\"\n//\n// The replacer caches computed values, so subsequent calls with the same\n// placeholder will reuse the cached value instead of executing the callback\n// again:\n//\n//\tr := dynreplacer.New()\n//\tr.RegisterCallback(\":expensive:\", func() string { return \"computed\" })\n//\tr.Replace(\"Value1: :expensive:\") // Computes the value\n//\tr.Replace(\"Value2: :expensive:\") // Uses cached value\n//\tr.ClearCache()                   // Force re-computation on next use\npackage dynreplacer\n\nimport (\n\t\"strings\"\n)\n\n// Replacer manages dynamic placeholders, their associated functions, and cached\n// values.\ntype Replacer struct {\n\tcallbacks    map[string]func() string\n\tcachedValues map[string]string\n}\n\n// Pair represents a placeholder and its callback function\ntype Pair struct {\n\tPlaceholder string\n\tCallback    func() string\n}\n\n// New creates a new Replacer instance with optional initial replacements.\n// It accepts pairs where each pair consists of a placeholder string and\n// its corresponding callback function.\n//\n// Example:\n//\n//\tNew(\n//\t    Pair{\":name:\", func() string { return \"World\" }},\n//\t    Pair{\":greeting:\", func() string { return \"Hello\" }},\n//\t)\nfunc New(pairs ...Pair) *Replacer {\n\tr := \u0026Replacer{\n\t\tcallbacks:    make(map[string]func() string),\n\t\tcachedValues: make(map[string]string),\n\t}\n\n\tfor _, pair := range pairs {\n\t\tr.RegisterCallback(pair.Placeholder, pair.Callback)\n\t}\n\n\treturn r\n}\n\n// RegisterCallback associates a placeholder with a function to generate its\n// content.\nfunc (r *Replacer) RegisterCallback(placeholder string, callback func() string) {\n\tr.callbacks[placeholder] = callback\n}\n\n// Replace processes the given layout, replacing placeholders with cached or\n// newly computed values.\nfunc (r *Replacer) Replace(layout string) string {\n\treplacements := []string{}\n\n\t// Check for placeholders and compute/retrieve values\n\thasReplacements := false\n\tfor placeholder, callback := range r.callbacks {\n\t\tif strings.Contains(layout, placeholder) {\n\t\t\tvalue, exists := r.cachedValues[placeholder]\n\t\t\tif !exists {\n\t\t\t\tvalue = callback()\n\t\t\t\tr.cachedValues[placeholder] = value\n\t\t\t}\n\t\t\treplacements = append(replacements, placeholder, value)\n\t\t\thasReplacements = true\n\t\t}\n\t}\n\n\t// If no replacements were found, return the original layout\n\tif !hasReplacements {\n\t\treturn layout\n\t}\n\n\t// Create a strings.Replacer with all computed replacements\n\treplacer := strings.NewReplacer(replacements...)\n\treturn replacer.Replace(layout)\n}\n\n// ClearCache clears all cached values, forcing re-computation on next Replace.\nfunc (r *Replacer) ClearCache() {\n\tr.cachedValues = make(map[string]string)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/dynreplacer\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"home","path":"gno.land/r/gnoland/home","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/home\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"home.gno","body":"package home\n\nimport (\n\t\"chain/runtime\"\n\t\"strconv\"\n\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/dynreplacer\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/r/devrels/events\"\n\tblog \"gno.land/r/gnoland/blog\"\n)\n\nvar (\n\toverride string\n\tAdmin    = ownable.NewWithAddress(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\") // govdao t1 multisig\n)\n\nfunc Render(_ string) string {\n\tr := dynreplacer.New()\n\tr.RegisterCallback(\":latest-blogposts:\", func() string {\n\t\treturn blog.RenderLastPostsWidget(4)\n\t})\n\tr.RegisterCallback(\":upcoming-events:\", func() string {\n\t\tout, _ := events.RenderEventWidget(events.MaxWidgetSize)\n\t\treturn out\n\t})\n\tr.RegisterCallback(\":qotb:\", quoteOfTheBlock)\n\tr.RegisterCallback(\":newsletter-button:\", newsletterButton)\n\tr.RegisterCallback(\":chain-height:\", func() string {\n\t\treturn strconv.Itoa(int(runtime.ChainHeight()))\n\t})\n\n\ttemplate := `# Welcome to Gno.land\n\nWe're building Gno.land, set to become the leading open-source smart contract\nplatform, using Gno, an interpreted and fully deterministic variation of the\nGo programming language for succinct and composable smart contracts.\n\nWith transparent and timeless code, Gno.land is the next generation of smart\ncontract platforms, serving as the \"GitHub\" of the ecosystem, with realms built\nusing fully transparent, auditable code that anyone can inspect and reuse.\n\nIntuitive and easy to use, Gno.land lowers the barrier to web3 and makes\ncensorship-resistant platforms accessible to everyone. If you want to help lay\nthe foundations of a fairer and freer world, join us today.\n\n---\n\n## [Boards](/r/gnoland/boards2/v1) - On-chain forum for the Gno.land community\n\n**Post, discuss, and create your content community**: Boards is a fully on-chain social forum to create Boards topics, post threads, comment and reply. A plug-and-deploy DAO lets communities manage content, permissions and moderation their way.\n\nExplore this ready-to-use Gno dApp, and experience decentralized social media in action.\n\n**[Open Boards](/r/gnoland/boards2/v1)**\n\n---\n\n\u003cgno-columns\u003e\n## Learn about Gno.land\n\n- [About](/about)\n- [GitHub](https://github.com/gnolang)\n- [Blog](/blog)\n- [Events](/events)\n- [Partners, Fund, Grants](/partners)\n- [Explore the Ecosystem](/ecosystem)\n- [Careers](https://jobs.ashbyhq.com/allinbits)\n\n\u003cgno-columns-sep\u003e\n\n## Build with Gno\n\n- [Write Gno in the browser](https://play.gno.land)\n- [Read about the Gno Language](/gnolang)\n- [Visit the official documentation](https://docs.gno.land)\n- [Efficient local development for Gno](https://docs.gno.land/resources/gnodev)\n- [Get testnet GNOTs](https://faucet.gno.land)\n\n\u003cgno-columns-sep\u003e\n\n## Explore the universe\n\n- [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n- [Gnoscan](https://gnoscan.io)\n- [Gno networks documentation](https://docs.gno.land/resources/gnoland-networks/)\n- [Staging](https://staging.gno.land/)\n- [Testnet 12](https://test12.testnets.gno.land/)\n- [Faucet Hub](https://faucet.gno.land)\n\n\u003c/gno-columns\u003e\n\n\u003cgno-columns\u003e\n\n## [Latest Blogposts](/r/gnoland/blog)\n\n:latest-blogposts:\n\n\u003cgno-columns-sep\u003e\n\n## [Latest Events](/events)\n\n:upcoming-events:\n\n\u003c/gno-columns\u003e\n\n---\n\n## [Gno Playground](https://play.gno.land)\n\nGno Playground is a web application designed for building, running, testing, and\ninteracting with your Gno code, enhancing your understanding of the Gno\nlanguage. With Gno Playground, you can share your code, execute tests, deploy\nyour realms and packages to Gno.land, and explore a multitude of other features.\n\nExperience the convenience of code sharing and rapid experimentation with\n[Gno Playground](https://play.gno.land).\n\n---\n\n## Explore New Packages and Realms\n\nAll code in Gno.land is organized in packages, and each package lives at a unique package path like\n\"r/gnoland/home\". You can browse packages, inspect their source, and use them in your own libraries and realms.\n\n\u003cgno-columns\u003e\n\n### r/gnoland\n\nOfficial realm packages developed by the Gno.land core team.\n\n[Browse](/r/gnoland)\n\n\u003cgno-columns-sep\u003e\n\n### r/sys\n\nSystem-level realm packages used by the chain.\n\n[Browse](/r/sys)\n\n\u003cgno-columns-sep\u003e\n\n### r/demo\n\nDemo realm packages showcasing what’s possible.\n\n[Browse](/r/demo)\n\n\u003cgno-columns-sep\u003e\n\n### p/demo\n\nPure packages for demo purposes.\n\n[Browse](/p/demo)\n\n\u003c/gno-columns\u003e\n\n---\n\n\u003cgno-columns\u003e\n\n## Socials\n\n- Check out our [community projects](https://github.com/gnolang/awesome-gno)\n- [Discord](https://discord.gg/S8nKUqwkPn)\n- [Twitter](https://twitter.com/_gnoland)\n- [Youtube](https://www.youtube.com/@_gnoland)\n- [Telegram](https://t.me/gnoland)\n\n\u003cgno-columns-sep\u003e\n\n## Quote of the ~Day~ Block #:chain-height:\n\n\u003e :qotb:\n\n\u003c/gno-columns\u003e\n\n---\n\n## Sign up for our newsletter\n\nStay in the Gno by signing up for our newsletter. You'll get the scoop on dev updates, fresh content, and community news.\n\n:newsletter-button:\n\n---\n\n**This is a testnet.** Package names are not guaranteed to be available for production.`\n\n\tif override != \"\" {\n\t\ttemplate = override\n\t}\n\tresult := r.Replace(template)\n\treturn result\n}\n\nfunc newsletterButton() string {\n\treturn svgbtn.Button(\n\t\t256,\n\t\t44,\n\t\t\"#226c57\",\n\t\t\"#ffffff\",\n\t\t\"Subscribe to stay in the Gno\",\n\t\t\"https://land.us18.list-manage.com/subscribe?u=8befe3303cf82796d2c1a1aff\u0026id=271812000b\",\n\t)\n}\n\nfunc quoteOfTheBlock() string {\n\tquotes := []string{\n\t\t\"Gno is for Truth.\",\n\t\t\"Gno is for Social Coordination.\",\n\t\t\"Gno is _not only_ for DeFi.\",\n\t\t\"Now, you Gno.\",\n\t\t\"Come for the Go, Stay for the Gno.\",\n\t}\n\theight := runtime.ChainHeight()\n\tidx := int(height) % len(quotes)\n\tqotb := quotes[idx]\n\treturn qotb\n}\n\nfunc AdminSetOverride(cur realm, content string) {\n\tAdmin.AssertOwnedBy(cur.Previous().Address())\n\toverride = content\n}\n\nfunc AdminTransferOwnership(cur realm, newOwner address) {\n\tif err := Admin.TransferOwnership(0, cur, newOwner); err != nil {\n\t\tpanic(err)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"loader","path":"gno.land/r/gov/dao/v3/loader","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/v3/loader\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"},{"name":"loader.gno","body":"// loader.gno initialises the govDAO v3 implementation and tier structure.\n//\n// It intentionally does NOT add any members or set AllowedDAOs.  When the\n// allowedDAOs list in the DAO proxy is empty, InAllowedDAOs() returns true\n// for any caller (see r/gov/dao/proxy.gno), which lets a subsequent MsgRun\n// bootstrap the member set and then lock things down.\n//\n// Bootstrap flow (official network genesis or local dev):\n//\n//  1. All packages — including this loader — are deployed via MsgAddPackage.\n//     The loader sets up tier entries and the DAO implementation.\n//  2. A MsgRun executes a setup script (e.g. govdao_prop1.gno) which:\n//     a. Adds a temporary deployer as T1 member (for supermajority).\n//     b. Creates a governance proposal to register validators, votes YES,\n//     and executes it.\n//     c. Adds the real govDAO members directly via memberstore.Get().\n//     d. Removes the temporary deployer.\n//     e. Calls dao.UpdateImpl to set AllowedDAOs, locking down access.\n//\n// See misc/deployments/ for concrete genesis generation examples.\npackage loader\n\nimport (\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/impl\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nfunc init(cur realm) {\n\t// Create tier entries in the members tree (required before any SetMember).\n\tmemberstore.Get(0, cur).SetTier(memberstore.T1)\n\tmemberstore.Get(0, cur).SetTier(memberstore.T2)\n\tmemberstore.Get(0, cur).SetTier(memberstore.T3)\n\n\t// Set the DAO implementation.  AllowedDAOs is intentionally left empty\n\t// so that the genesis MsgRun can manipulate the memberstore directly.\n\tdao.UpdateImpl(cross(cur), dao.NewUpdateRequest(impl.GetInstance(0, cur), nil))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"diff","path":"gno.land/p/onbloc/diff","files":[{"name":"diff.gno","body":"// The diff package implements the Myers diff algorithm to compute the edit distance\n// and generate a minimal edit script between two strings.\n//\n// Edit distance, also known as Levenshtein distance, is a measure of the similarity\n// between two strings. It is defined as the minimum number of single-character edits (insertions,\n// deletions, or substitutions) required to change one string into the other.\npackage diff\n\nimport (\n\t\"strings\"\n)\n\n// EditType represents the type of edit operation in a diff.\ntype EditType uint8\n\nconst (\n\t// EditKeep indicates that a character is unchanged in both strings.\n\tEditKeep EditType = iota\n\n\t// EditInsert indicates that a character was inserted in the new string.\n\tEditInsert\n\n\t// EditDelete indicates that a character was deleted from the old string.\n\tEditDelete\n)\n\n// Edit represent a single edit operation in a diff.\ntype Edit struct {\n\t// Type is the kind of edit operation.\n\tType EditType\n\n\t// Char is the character involved in the edit operation.\n\tChar rune\n}\n\n// MyersDiff computes the difference between two strings using Myers' diff algorithm.\n// It returns a slice of Edit operations that transform the old string into the new string.\n// This implementation finds the shortest edit script (SES) that represents the minimal\n// set of operations to transform one string into the other.\n//\n// The function handles both ASCII and non-ASCII characters correctly.\n//\n// Time complexity: O((N+M)D), where N and M are the lengths of the input strings,\n// and D is the size of the minimum edit script.\n//\n// Space complexity: O((N+M)D)\n//\n// In the worst case, where the strings are completely different, D can be as large as N+M,\n// leading to a time and space complexity of O((N+M)^2). However, for strings with many\n// common substrings, the performance is much better, often closer to O(N+M).\n//\n// Parameters:\n//   - old: the original string.\n//   - new: the modified string.\n//\n// Returns:\n//   - A slice of Edit operations representing the minimum difference between the two strings.\nfunc MyersDiff(old, new string) []Edit {\n\toldRunes, newRunes := []rune(old), []rune(new)\n\tn, m := len(oldRunes), len(newRunes)\n\n\tif n == 0 \u0026\u0026 m == 0 {\n\t\treturn []Edit{}\n\t}\n\n\t// old is empty\n\tif n == 0 {\n\t\tedits := make([]Edit, m)\n\t\tfor i, r := range newRunes {\n\t\t\tedits[i] = Edit{Type: EditInsert, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tif m == 0 {\n\t\tedits := make([]Edit, n)\n\t\tfor i, r := range oldRunes {\n\t\t\tedits[i] = Edit{Type: EditDelete, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tmax := n + m\n\tv := make([]int, 2*max+1)\n\tvar trace [][]int\nsearch:\n\tfor d := 0; d \u003c= max; d++ {\n\t\t// iterate through diagonals\n\t\tfor k := -d; k \u003c= d; k += 2 {\n\t\t\tvar x int\n\t\t\tif k == -d || (k != d \u0026\u0026 v[max+k-1] \u003c v[max+k+1]) {\n\t\t\t\tx = v[max+k+1] // move down\n\t\t\t} else {\n\t\t\t\tx = v[max+k-1] + 1 // move right\n\t\t\t}\n\t\t\ty := x - k\n\n\t\t\t// extend the path as far as possible with matching characters\n\t\t\tfor x \u003c n \u0026\u0026 y \u003c m \u0026\u0026 oldRunes[x] == newRunes[y] {\n\t\t\t\tx++\n\t\t\t\ty++\n\t\t\t}\n\n\t\t\tv[max+k] = x\n\n\t\t\t// check if we've reached the end of both strings\n\t\t\tif x == n \u0026\u0026 y == m {\n\t\t\t\ttrace = append(trace, append([]int(nil), v...))\n\t\t\t\tbreak search\n\t\t\t}\n\t\t}\n\t\ttrace = append(trace, append([]int(nil), v...))\n\t}\n\n\t// backtrack to construct the edit script\n\tedits := make([]Edit, 0, n+m)\n\tx, y := n, m\n\tfor d := len(trace) - 1; d \u003e= 0; d-- {\n\t\tvPrev := trace[d]\n\t\tk := x - y\n\t\tvar prevK int\n\t\tif k == -d || (k != d \u0026\u0026 vPrev[max+k-1] \u003c vPrev[max+k+1]) {\n\t\t\tprevK = k + 1\n\t\t} else {\n\t\t\tprevK = k - 1\n\t\t}\n\t\tprevX := vPrev[max+prevK]\n\t\tprevY := prevX - prevK\n\n\t\t// add keep edits for matching characters\n\t\tfor x \u003e prevX \u0026\u0026 y \u003e prevY {\n\t\t\tif x \u003e 0 \u0026\u0026 y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditKeep, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t\ty--\n\t\t}\n\t\tif y \u003e prevY {\n\t\t\tif y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditInsert, Char: newRunes[y-1]}}, edits...)\n\t\t\t}\n\t\t\ty--\n\t\t} else if x \u003e prevX {\n\t\t\tif x \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditDelete, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t}\n\t}\n\n\treturn edits\n}\n\n// Format converts a slice of Edit operations into a human-readable string representation.\n// It groups consecutive edits of the same type and formats them as follows:\n//   - Unchanged characters are left as-is\n//   - Inserted characters are wrapped in [+...]\n//   - Deleted characters are wrapped in [-...]\n//\n// This function is useful for visualizing the differences between two strings\n// in a compact and intuitive format.\n//\n// Parameters:\n//   - edits: A slice of Edit operations, typically produced by MyersDiff\n//\n// Returns:\n//   - A formatted string representing the diff\n//\n// Example output:\n//\n//\tFor the diff between \"abcd\" and \"acbd\", the output might be:\n//\t\"a[-b]c[+b]d\"\n//\n// Note:\n//\n//\tThe function assumes that the input slice of edits is in the correct order.\n//\tAn empty input slice will result in an empty string.\nfunc Format(edits []Edit) string {\n\tif len(edits) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar (\n\t\tresult       strings.Builder\n\t\tcurrentType  EditType\n\t\tcurrentChars strings.Builder\n\t)\n\n\tflushCurrent := func() {\n\t\tif currentChars.Len() \u003e 0 {\n\t\t\tswitch currentType {\n\t\t\tcase EditKeep:\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\tcase EditInsert:\n\t\t\t\tresult.WriteString(\"[+\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\tcase EditDelete:\n\t\t\t\tresult.WriteString(\"[-\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\t}\n\t\t\tcurrentChars.Reset()\n\t\t}\n\t}\n\n\tfor _, edit := range edits {\n\t\tif edit.Type != currentType {\n\t\t\tflushCurrent()\n\t\t\tcurrentType = edit.Type\n\t\t}\n\t\tcurrentChars.WriteRune(edit.Char)\n\t}\n\tflushCurrent()\n\n\treturn result.String()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/onbloc/diff\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"uassert","path":"gno.land/p/nt/uassert/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `uassert` - test assertions\n\nAssertion helpers for writing Gno tests, in both `_test.gno` and `_filetest.gno` files. Adapted, lighter port of `stretchr/testify/assert`. Each helper takes a `TestingT`, reports the failure via `t.Errorf`, and lets the test keep running.\n\n## Usage\n\n```go\nimport (\n    \"testing\"\n\n    \"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestAdd(cur realm, t *testing.T) {\n    got, err := Add(2, 3)\n    uassert.NoError(t, err)\n    uassert.Equal(t, 5, got)\n    uassert.True(t, got \u003e 0, \"result must be positive\")\n\n    uassert.PanicsWithMessage(t, cur, \"div by zero\", func() {\n        Div(1, 0)\n    })\n}\n```\n\nEvery helper returns a `bool` (`true` on success) so they can be chained or used in conditionals.\n\n## API\n\n```go\ntype TestingT interface {\n    Helper()\n    Skip(args ...any)\n    Fatalf(fmt string, args ...any)\n    Errorf(fmt string, args ...any)\n    Logf(fmt string, args ...any)\n    Fail()\n    FailNow()\n}\n```\n\nEquality and emptiness (supports `string`, `address`, `bool`, all int/uint widths, `float32/64`):\n\n```go\nfunc Equal(t TestingT, expected, actual any, msgs ...string) bool\nfunc NotEqual(t TestingT, expected, actual any, msgs ...string) bool\nfunc Empty(t TestingT, obj any, msgs ...string) bool\nfunc NotEmpty(t TestingT, obj any, msgs ...string) bool\n```\n\nTruthiness and nil:\n\n```go\nfunc True(t TestingT, value bool, msgs ...string) bool\nfunc False(t TestingT, value bool, msgs ...string) bool\nfunc Nil(t TestingT, value any, msgs ...string) bool\nfunc NotNil(t TestingT, value any, msgs ...string) bool\nfunc TypedNil(t TestingT, value any, msgs ...string) bool\nfunc NotTypedNil(t TestingT, value any, msgs ...string) bool\n```\n\nErrors:\n\n```go\nfunc NoError(t TestingT, err error, msgs ...string) bool\nfunc Error(t TestingT, err error, msgs ...string) bool\nfunc ErrorContains(t TestingT, err error, contains string, msgs ...string) bool\nfunc ErrorIs(t TestingT, err, target error, msgs ...string) bool\n```\n\nPanics and aborts (`f` may be `func()` or `func(realm)`; pass the test's own `cur` as `rlm`):\n\n```go\nfunc PanicsWithMessage(t TestingT, rlm realm, msg string, f any, msgs ...string) bool\nfunc PanicsContains(t TestingT, rlm realm, substr string, f any, msgs ...string) bool\nfunc NotPanics(t TestingT, rlm realm, f any, msgs ...string) bool\nfunc AbortsWithMessage(t TestingT, rlm realm, msg string, f any, msgs ...string) bool\nfunc AbortsContains(t TestingT, rlm realm, substr string, f any, msgs ...string) bool\nfunc NotAborts(t TestingT, rlm realm, f any, msgs ...string) bool\n```\n\n## Notes\n\n- A *panic* is a same-realm runtime failure caught with `recover`. An *abort* is a panic that crosses a realm boundary, caught with gno's `revive`. Use the right variant: `PanicsX` for same-realm, `AbortsX` for cross-realm. `NotPanics` covers both.\n- `uassert` reports the failure but lets the test continue. Use `gno.land/p/nt/urequire/v0` when subsequent assertions wouldn't be meaningful after a failure.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\npackage uassert // import \"gno.land/p/nt/uassert/v0\"\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/uassert/v0\"\ngno = \"0.9\"\n"},{"name":"helpers.gno","body":"package uassert\n\nimport \"strings\"\n\nfunc fail(t TestingT, customMsgs []string, failureMessage string, args ...any) bool {\n\tcustomMsg := \"\"\n\tif len(customMsgs) \u003e 0 {\n\t\tcustomMsg = strings.Join(customMsgs, \" \")\n\t}\n\tif customMsg != \"\" {\n\t\tfailureMessage += \" - \" + customMsg\n\t}\n\tt.Errorf(failureMessage, args...)\n\treturn false\n}\n\nfunc checkDidPanic(f any, rlm realm) (didPanic bool, message string) {\n\tdidPanic = true\n\tdefer func() {\n\t\tr := recover()\n\n\t\tif r == nil {\n\t\t\tmessage = \"nil\"\n\t\t\treturn\n\t\t}\n\n\t\terr, ok := r.(error)\n\t\tif ok {\n\t\t\tmessage = err.Error()\n\t\t\treturn\n\t\t}\n\n\t\terrStr, ok := r.(string)\n\t\tif ok {\n\t\t\tmessage = errStr\n\t\t\treturn\n\t\t}\n\n\t\tmessage = \"recover: unsupported type\"\n\t}()\n\tswitch f := f.(type) {\n\tcase func():\n\t\tf()\n\tcase func(realm):\n\t\tf(cross(rlm))\n\tdefault:\n\t\tpanic(\"f must be of type func() or func(realm)\")\n\t}\n\tdidPanic = false\n\treturn\n}\n"},{"name":"types.gno","body":"package uassert\n\ntype TestingT interface {\n\tHelper()\n\tSkip(args ...any)\n\tFatalf(fmt string, args ...any)\n\tErrorf(fmt string, args ...any)\n\tLogf(fmt string, args ...any)\n\tFail()\n\tFailNow()\n}\n"},{"name":"uassert.gno","body":"// uassert is an adapted lighter version of https://github.com/stretchr/testify/assert.\npackage uassert\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/onbloc/diff\"\n)\n\n// NoError asserts that a function returned no error (i.e. `nil`).\nfunc NoError(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err != nil {\n\t\treturn fail(t, msgs, \"unexpected error: %s\", err.Error())\n\t}\n\treturn true\n}\n\n// Error asserts that a function returned an error (i.e. not `nil`).\nfunc Error(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err == nil {\n\t\treturn fail(t, msgs, \"an error is expected but got nil\")\n\t}\n\treturn true\n}\n\n// ErrorContains asserts that a function returned an error (i.e. not `nil`)\n// and that the error contains the specified substring.\nfunc ErrorContains(t TestingT, err error, contains string, msgs ...string) bool {\n\tt.Helper()\n\n\tif !Error(t, err, msgs...) {\n\t\treturn false\n\t}\n\n\tactual := err.Error()\n\tif !strings.Contains(actual, contains) {\n\t\treturn fail(t, msgs, \"error %q does not contain %q\", actual, contains)\n\t}\n\n\treturn true\n}\n\n// True asserts that the specified value is true.\nfunc True(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif !value {\n\t\treturn fail(t, msgs, \"should be true\")\n\t}\n\treturn true\n}\n\n// False asserts that the specified value is false.\nfunc False(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif value {\n\t\treturn fail(t, msgs, \"should be false\")\n\t}\n\treturn true\n}\n\n// ErrorIs asserts the given error matches the target error using errors.Is,\n// which traverses the error chain looking for a match.\nfunc ErrorIs(t TestingT, err, target error, msgs ...string) bool {\n\tt.Helper()\n\n\tif !errors.Is(err, target) {\n\t\treturn fail(t, msgs, \"error mismatch, expected %s, got %s\", target, err)\n\t}\n\n\treturn true\n}\n\n// AbortsWithMessage asserts that the code inside the specified func aborts\n// (panics when crossing another realm).\n// Use PanicsWithMessage for asserting local panics within the same realm.\n//\n// `rlm` is threaded into the callback via cross(rlm) when f is func(realm).\n// It is ignored for func() callbacks. /p/ production code cannot declare\n// crossing functions, so rlm is taken as the second (non-first) parameter\n// — callers pass `cur` directly.\n//\n// NOTE: This relies on gno's `revive` mechanism to catch aborts.\nfunc AbortsWithMessage(t TestingT, rlm realm, msg string, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tvar didAbort bool\n\tvar abortValue any\n\tvar r any\n\n\tswitch f := f.(type) {\n\tcase func():\n\t\tr = revive(f) // revive() captures the value passed to panic()\n\tcase func(realm):\n\t\tr = revive(func() { f(cross(rlm)) })\n\tdefault:\n\t\tpanic(\"f must be of type func() or func(realm)\")\n\t}\n\tif r != nil {\n\t\tdidAbort = true\n\t\tabortValue = r\n\t}\n\n\tif !didAbort {\n\t\t// If the function didn't abort as expected\n\t\treturn fail(t, msgs, \"func should abort\")\n\t}\n\n\t// Check if the abort value matches the expected message string\n\tabortStr := ufmt.Sprintf(\"%v\", abortValue)\n\tif abortStr != msg {\n\t\treturn fail(t, msgs, \"func should abort with message:\\t%q\\n\\tActual abort value:\\t%q\", msg, abortStr)\n\t}\n\n\t// Success: function aborted with the expected message\n\treturn true\n}\n\n// AbortsContains asserts that the code inside the specified func aborts\n// (panics when crossing another realm) and the abort message contains the specified substring.\n// See AbortsWithMessage for `rlm` semantics.\nfunc AbortsContains(t TestingT, rlm realm, substr string, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tvar didAbort bool\n\tvar abortValue any\n\tvar r any\n\n\tif fn, ok := f.(func()); ok {\n\t\tr = revive(fn)\n\t} else if fn, ok := f.(func(realm)); ok {\n\t\tr = revive(func() { fn(cross(rlm)) })\n\t} else {\n\t\tpanic(\"f must be of type func() or func(realm)\")\n\t}\n\tif r != nil {\n\t\tdidAbort = true\n\t\tabortValue = r\n\t}\n\n\tif !didAbort {\n\t\treturn fail(t, msgs, \"func should abort\")\n\t}\n\n\tabortStr := ufmt.Sprintf(\"%v\", abortValue)\n\tif !strings.Contains(abortStr, substr) {\n\t\treturn fail(t, msgs, \"func should abort with message containing:\\t%q\\n\\tActual abort value:\\t%q\", substr, abortStr)\n\t}\n\n\treturn true\n}\n\n// NotAborts asserts that the code inside the specified func does NOT abort\n// when crossing an execution boundary.\n// Note: Consider using NotPanics which checks for both panics and aborts.\n// See AbortsWithMessage for `rlm` semantics.\nfunc NotAborts(t TestingT, rlm realm, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tvar didAbort bool\n\tvar abortValue any\n\tvar r any\n\n\tswitch f := f.(type) {\n\tcase func():\n\t\tr = revive(f) // revive() captures the value passed to panic()\n\tcase func(realm):\n\t\tr = revive(func() { f(cross(rlm)) })\n\tdefault:\n\t\tpanic(\"f must be of type func() or func(realm)\")\n\t}\n\tif r != nil {\n\t\tdidAbort = true\n\t\tabortValue = r\n\t}\n\n\tif didAbort {\n\t\t// Fail if the function aborted when it shouldn't have\n\t\t// Attempt to format the abort value in the error message\n\t\treturn fail(t, msgs, \"func should not abort\\\\n\\\\tAbort value:\\\\t%v\", abortValue)\n\t}\n\n\t// Success: function did not abort\n\treturn true\n}\n\n// PanicsWithMessage asserts that the code inside the specified func panics\n// locally within the same execution realm.\n// Use AbortsWithMessage for asserting panics that cross execution boundaries (aborts).\n// See AbortsWithMessage for `rlm` semantics.\nfunc PanicsWithMessage(t TestingT, rlm realm, msg string, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f, rlm)\n\tif !didPanic {\n\t\treturn fail(t, msgs, \"func should panic\\n\\tPanic value:\\t%v\", panicValue)\n\t}\n\n\t// Check if the abort value matches the expected message string\n\tpanicStr := ufmt.Sprintf(\"%v\", panicValue)\n\tif panicStr != msg {\n\t\treturn fail(t, msgs, \"func should panic with message:\\t%q\\n\\tActual panic value:\\t%q\", msg, panicStr)\n\t}\n\treturn true\n}\n\n// PanicsContains asserts that the code inside the specified func panics\n// locally within the same execution realm and the panic message contains the specified substring.\n// See AbortsWithMessage for `rlm` semantics.\nfunc PanicsContains(t TestingT, rlm realm, substr string, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f, rlm)\n\tif !didPanic {\n\t\treturn fail(t, msgs, \"func should panic\\n\\tPanic value:\\t%v\", panicValue)\n\t}\n\n\tpanicStr := ufmt.Sprintf(\"%v\", panicValue)\n\tif !strings.Contains(panicStr, substr) {\n\t\treturn fail(t, msgs, \"func should panic with message containing:\\t%q\\n\\tActual panic value:\\t%q\", substr, panicStr)\n\t}\n\treturn true\n}\n\n// NotPanics asserts that the code inside the specified func does NOT panic\n// (within the same realm) or abort (due to a cross-realm panic).\n// See AbortsWithMessage for `rlm` semantics.\nfunc NotPanics(t TestingT, rlm realm, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tvar panicVal any\n\tvar didPanic bool\n\tvar abortVal any\n\n\t// Use revive to catch cross-realm aborts\n\tabortVal = revive(func() {\n\t\t// Use defer+recover to catch same-realm panics\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tdidPanic = true\n\t\t\t\tpanicVal = r\n\t\t\t}\n\t\t}()\n\t\t// Execute the function\n\t\tswitch f := f.(type) {\n\t\tcase func():\n\t\t\tf()\n\t\tcase func(realm):\n\t\t\tf(cross(rlm))\n\t\tdefault:\n\t\t\tpanic(\"f must be of type func() or func(realm)\")\n\t\t}\n\t})\n\n\t// Check if revive caught an abort\n\tif abortVal != nil {\n\t\treturn fail(t, msgs, \"func should not abort\\n\\tAbort value:\\t%+v\", abortVal)\n\t}\n\n\t// Check if recover caught a panic\n\tif didPanic {\n\t\t// Format panic value for message\n\t\tpanicMsg := \"\"\n\t\tif panicVal == nil {\n\t\t\tpanicMsg = \"nil\"\n\t\t} else if err, ok := panicVal.(error); ok {\n\t\t\tpanicMsg = err.Error()\n\t\t} else if str, ok := panicVal.(string); ok {\n\t\t\tpanicMsg = str\n\t\t} else {\n\t\t\t// Fallback for other types\n\t\t\tpanicMsg = \"panic: unsupported type\"\n\t\t}\n\t\treturn fail(t, msgs, \"func should not panic\\n\\tPanic value:\\t%s\", panicMsg)\n\t}\n\n\treturn true // No panic or abort occurred\n}\n\n// Equal asserts that two objects are equal.\nfunc Equal(t TestingT, expected, actual any, msgs ...string) bool {\n\tt.Helper()\n\n\tif expected == nil || actual == nil {\n\t\treturn expected == actual\n\t}\n\n\t// XXX: errors\n\t// XXX: slices\n\t// XXX: pointers\n\n\tequal := false\n\tok_ := false\n\tes, as := \"unsupported type\", \"unsupported type\"\n\n\tswitch ev := expected.(type) {\n\tcase string:\n\t\tif av, ok := actual.(string); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = ev, av\n\t\t\tif !equal {\n\t\t\t\tdif := diff.MyersDiff(ev, av)\n\t\t\t\treturn fail(t, msgs, \"uassert.Equal: strings are different\\n\\tDiff: %s\", diff.Format(dif))\n\t\t\t}\n\t\t}\n\tcase address:\n\t\tif av, ok := actual.(address); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = string(ev), string(av)\n\t\t}\n\tcase int:\n\t\tif av, ok := actual.(int); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(ev), strconv.Itoa(av)\n\t\t}\n\tcase int8:\n\t\tif av, ok := actual.(int8); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int16:\n\t\tif av, ok := actual.(int16); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int32:\n\t\tif av, ok := actual.(int32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int64:\n\t\tif av, ok := actual.(int64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase uint:\n\t\tif av, ok := actual.(uint); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint8:\n\t\tif av, ok := actual.(uint8); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint16:\n\t\tif av, ok := actual.(uint16); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint32:\n\t\tif av, ok := actual.(uint32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint64:\n\t\tif av, ok := actual.(uint64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)\n\t\t}\n\tcase bool:\n\t\tif av, ok := actual.(bool); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t\tif ev {\n\t\t\t\tes, as = \"true\", \"false\"\n\t\t\t} else {\n\t\t\t\tes, as = \"false\", \"true\"\n\t\t\t}\n\t\t}\n\tcase float32:\n\t\tif av, ok := actual.(float32); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t}\n\tcase float64:\n\t\tif av, ok := actual.(float64); ok {\n\t\t\tequal = ev == av\n\t\t\tok_ = true\n\t\t}\n\tdefault:\n\t\treturn fail(t, msgs, \"uassert.Equal: unsupported type\")\n\t}\n\n\t/*\n\t\t// XXX: implement stringer and other well known similar interfaces\n\t\ttype stringer interface{ String() string }\n\t\tif ev, ok := expected.(stringer); ok {\n\t\t\tif av, ok := actual.(stringer); ok {\n\t\t\t\tequal = ev.String() == av.String()\n\t\t\t\tok_ = true\n\t\t\t}\n\t\t}\n\t*/\n\n\tif !ok_ {\n\t\treturn fail(t, msgs, \"uassert.Equal: different types\") // XXX: display the types\n\t}\n\tif !equal {\n\t\treturn fail(t, msgs, \"uassert.Equal: same type but different value\\n\\texpected: %s\\n\\tactual:   %s\", es, as)\n\t}\n\n\treturn true\n}\n\n// NotEqual asserts that two objects are not equal.\nfunc NotEqual(t TestingT, expected, actual any, msgs ...string) bool {\n\tt.Helper()\n\n\tif expected == nil || actual == nil {\n\t\treturn expected != actual\n\t}\n\n\t// XXX: errors\n\t// XXX: slices\n\t// XXX: pointers\n\n\tnotEqual := false\n\tok_ := false\n\tes, as := \"unsupported type\", \"unsupported type\"\n\n\tswitch ev := expected.(type) {\n\tcase string:\n\t\tif av, ok := actual.(string); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = ev, av\n\t\t}\n\tcase address:\n\t\tif av, ok := actual.(address); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = string(ev), string(av)\n\t\t}\n\tcase int:\n\t\tif av, ok := actual.(int); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(ev), strconv.Itoa(av)\n\t\t}\n\tcase int8:\n\t\tif av, ok := actual.(int8); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int16:\n\t\tif av, ok := actual.(int16); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int32:\n\t\tif av, ok := actual.(int32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase int64:\n\t\tif av, ok := actual.(int64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))\n\t\t}\n\tcase uint:\n\t\tif av, ok := actual.(uint); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint8:\n\t\tif av, ok := actual.(uint8); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint16:\n\t\tif av, ok := actual.(uint16); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint32:\n\t\tif av, ok := actual.(uint32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)\n\t\t}\n\tcase uint64:\n\t\tif av, ok := actual.(uint64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tes, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)\n\t\t}\n\tcase bool:\n\t\tif av, ok := actual.(bool); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t\tif ev {\n\t\t\t\tes, as = \"true\", \"false\"\n\t\t\t} else {\n\t\t\t\tes, as = \"false\", \"true\"\n\t\t\t}\n\t\t}\n\tcase float32:\n\t\tif av, ok := actual.(float32); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t}\n\tcase float64:\n\t\tif av, ok := actual.(float64); ok {\n\t\t\tnotEqual = ev != av\n\t\t\tok_ = true\n\t\t}\n\tdefault:\n\t\treturn fail(t, msgs, \"uassert.NotEqual: unsupported type\")\n\t}\n\n\t/*\n\t\t// XXX: implement stringer and other well known similar interfaces\n\t\ttype stringer interface{ String() string }\n\t\tif ev, ok := expected.(stringer); ok {\n\t\t\tif av, ok := actual.(stringer); ok {\n\t\t\t\tnotEqual = ev.String() != av.String()\n\t\t\t\tok_ = true\n\t\t\t}\n\t\t}\n\t*/\n\n\tif !ok_ {\n\t\treturn fail(t, msgs, \"uassert.NotEqual: different types\") // XXX: display the types\n\t}\n\tif !notEqual {\n\t\treturn fail(t, msgs, \"uassert.NotEqual: same type and same value\\n\\texpected: %s\\n\\tactual:   %s\", es, as)\n\t}\n\n\treturn true\n}\n\nfunc isNumberEmpty(n any) (isNumber, isEmpty bool) {\n\tswitch n := n.(type) {\n\t// NOTE: the cases are split individually, so that n becomes of the\n\t// asserted type; the type of '0' was correctly inferred and converted\n\t// to the corresponding type, int, int8, etc.\n\tcase int:\n\t\treturn true, n == 0\n\tcase int8:\n\t\treturn true, n == 0\n\tcase int16:\n\t\treturn true, n == 0\n\tcase int32:\n\t\treturn true, n == 0\n\tcase int64:\n\t\treturn true, n == 0\n\tcase uint:\n\t\treturn true, n == 0\n\tcase uint8:\n\t\treturn true, n == 0\n\tcase uint16:\n\t\treturn true, n == 0\n\tcase uint32:\n\t\treturn true, n == 0\n\tcase uint64:\n\t\treturn true, n == 0\n\tcase float32:\n\t\treturn true, n == 0\n\tcase float64:\n\t\treturn true, n == 0\n\t}\n\treturn false, false\n}\n\nfunc Empty(t TestingT, obj any, msgs ...string) bool {\n\tt.Helper()\n\n\tisNumber, isEmpty := isNumberEmpty(obj)\n\tif isNumber {\n\t\tif !isEmpty {\n\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty number: %d\", obj)\n\t\t}\n\t} else {\n\t\tswitch val := obj.(type) {\n\t\tcase string:\n\t\t\tif val != \"\" {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty string: %s\", val)\n\t\t\t}\n\t\tcase address:\n\t\t\tvar zeroAddr address\n\t\t\tif val != zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty address: %s\", string(val))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fail(t, msgs, \"uassert.Empty: unsupported type\")\n\t\t}\n\t}\n\treturn true\n}\n\nfunc NotEmpty(t TestingT, obj any, msgs ...string) bool {\n\tt.Helper()\n\tisNumber, isEmpty := isNumberEmpty(obj)\n\tif isNumber {\n\t\tif isEmpty {\n\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty number: %d\", obj)\n\t\t}\n\t} else {\n\t\tswitch val := obj.(type) {\n\t\tcase string:\n\t\t\tif val == \"\" {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty string: %s\", val)\n\t\t\t}\n\t\tcase address:\n\t\t\tvar zeroAddr address\n\t\t\tif val == zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty address: %s\", string(val))\n\t\t\t}\n\t\tdefault:\n\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: unsupported type\")\n\t\t}\n\t}\n\treturn true\n}\n\n// Nil asserts that the value is nil.\nfunc Nil(t TestingT, value any, msgs ...string) bool {\n\tt.Helper()\n\tif value != nil {\n\t\treturn fail(t, msgs, \"should be nil\")\n\t}\n\treturn true\n}\n\n// NotNil asserts that the value is not nil.\nfunc NotNil(t TestingT, value any, msgs ...string) bool {\n\tt.Helper()\n\tif value == nil {\n\t\treturn fail(t, msgs, \"should not be nil\")\n\t}\n\treturn true\n}\n\n// TypedNil asserts that the value is a typed-nil (nil pointer) value.\nfunc TypedNil(t TestingT, value any, msgs ...string) bool {\n\tt.Helper()\n\tif value == nil {\n\t\treturn fail(t, msgs, \"should be typed-nil but got nil instead\")\n\t}\n\tif !istypednil(value) {\n\t\treturn fail(t, msgs, \"should be typed-nil\")\n\t}\n\treturn true\n}\n\n// NotTypedNil asserts that the value is not a typed-nil (nil pointer) value.\nfunc NotTypedNil(t TestingT, value any, msgs ...string) bool {\n\tt.Helper()\n\tif istypednil(value) {\n\t\treturn fail(t, msgs, \"should not be typed-nil\")\n\t}\n\treturn true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"urequire","path":"gno.land/p/nt/urequire/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# `urequire` - fail-fast test assertions\n\nSister package to `uassert`. Same assertions, but each one calls `t.FailNow()` on failure so the test stops immediately instead of continuing.\n\n## Usage\n\n```go\nimport (\n    \"testing\"\n\n    \"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestPipeline(t *testing.T) {\n    out, err := Build()\n    urequire.NoError(t, err)        // aborts the test if Build failed\n    urequire.NotNil(t, out)         // out is safe to dereference below\n    urequire.Equal(t, \"ready\", out.Status)\n}\n```\n\n## API\n\nHelpers take `uassert.TestingT` and return nothing — they either pass or stop the test.\n\nEquality and emptiness:\n\n```go\nfunc Equal(t uassert.TestingT, expected, actual any, msgs ...string)\nfunc NotEqual(t uassert.TestingT, expected, actual any, msgs ...string)\nfunc Empty(t uassert.TestingT, obj any, msgs ...string)\nfunc NotEmpty(t uassert.TestingT, obj any, msgs ...string)\n```\n\nTruthiness and nil:\n\n```go\nfunc True(t uassert.TestingT, value bool, msgs ...string)\nfunc False(t uassert.TestingT, value bool, msgs ...string)\nfunc Nil(t uassert.TestingT, value any, msgs ...string)\nfunc NotNil(t uassert.TestingT, value any, msgs ...string)\nfunc TypedNil(t uassert.TestingT, value any, msgs ...string)\nfunc NotTypedNil(t uassert.TestingT, value any, msgs ...string)\n```\n\nErrors:\n\n```go\nfunc NoError(t uassert.TestingT, err error, msgs ...string)\nfunc Error(t uassert.TestingT, err error, msgs ...string)\nfunc ErrorContains(t uassert.TestingT, err error, contains string, msgs ...string)\nfunc ErrorIs(t uassert.TestingT, err, target error, msgs ...string)\n```\n\nPanics and aborts (`f` may be `func()` or `func(realm)`; pass the test's own `cur` as `rlm`):\n\n```go\nfunc PanicsWithMessage(t uassert.TestingT, rlm realm, msg string, f any, msgs ...string)\nfunc PanicsContains(t uassert.TestingT, rlm realm, substr string, f any, msgs ...string)\nfunc NotPanics(t uassert.TestingT, rlm realm, f any, msgs ...string)\nfunc AbortsWithMessage(t uassert.TestingT, rlm realm, msg string, f any, msgs ...string)\nfunc AbortsContains(t uassert.TestingT, rlm realm, substr string, f any, msgs ...string)\nfunc NotAborts(t uassert.TestingT, rlm realm, f any, msgs ...string)\n```\n\n## Notes\n\n- Use `urequire` when the rest of the test depends on the assertion holding (e.g. a `nil` check before dereferencing). Use `uassert` when you want to collect multiple failures from the same test run.\n- Each `urequire` helper is a thin wrapper that calls the matching `uassert` helper and then `t.FailNow()` on failure.\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package urequire provides test assertion functions that immediately fail the\n// test on error, complementing the uassert package.\npackage urequire\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/urequire/v0\"\ngno = \"0.9\"\n"},{"name":"urequire.gno","body":"// urequire is a sister package for uassert.\n// XXX: codegen the package.\npackage urequire\n\nimport \"gno.land/p/nt/uassert/v0\"\n\n// type TestingT = uassert.TestingT // XXX: bug, should work\n\n// NoError requires that a function returned no error (i.e. `nil`).\nfunc NoError(t uassert.TestingT, err error, msgs ...string) {\n\tt.Helper()\n\tif uassert.NoError(t, err, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// Error requires that a function returned an error (i.e. not `nil`).\nfunc Error(t uassert.TestingT, err error, msgs ...string) {\n\tt.Helper()\n\tif uassert.Error(t, err, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// ErrorContains requires that a function returned an error (i.e. not `nil`)\n// and that the error contains the specified substring.\nfunc ErrorContains(t uassert.TestingT, err error, contains string, msgs ...string) {\n\tt.Helper()\n\tif uassert.ErrorContains(t, err, contains, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// True requires that the specified value is true.\nfunc True(t uassert.TestingT, value bool, msgs ...string) {\n\tt.Helper()\n\tif uassert.True(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// False requires that the specified value is false.\nfunc False(t uassert.TestingT, value bool, msgs ...string) {\n\tt.Helper()\n\tif uassert.False(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// ErrorIs requires that the given error matches the target error.\nfunc ErrorIs(t uassert.TestingT, err, target error, msgs ...string) {\n\tt.Helper()\n\tif uassert.ErrorIs(t, err, target, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// AbortsWithMessage requires that the code inside the specified func aborts\n// (panics when crossing another realm).\n// Use PanicsWithMessage for requiring local panics within the same realm.\n// Note: This relies on gno's `revive` mechanism to catch aborts.\n// See uassert.AbortsWithMessage for `rlm` semantics.\nfunc AbortsWithMessage(t uassert.TestingT, rlm realm, msg string, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.AbortsWithMessage(t, rlm, msg, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// AbortsContains requires that the code inside the specified func aborts\n// (panics when crossing another realm) and the abort message contains the specified substring.\n// See uassert.AbortsWithMessage for `rlm` semantics.\nfunc AbortsContains(t uassert.TestingT, rlm realm, substr string, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.AbortsContains(t, rlm, substr, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// NotAborts requires that the code inside the specified func does NOT abort\n// when crossing an execution boundary (e.g., VM call).\n// Use NotPanics for requiring the absence of local panics within the same realm.\n// Note: This relies on Gno's `revive` mechanism.\n// See uassert.AbortsWithMessage for `rlm` semantics.\nfunc NotAborts(t uassert.TestingT, rlm realm, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotAborts(t, rlm, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// PanicsWithMessage requires that the code inside the specified func panics\n// locally within the same execution realm.\n// Use AbortsWithMessage for requiring panics that cross execution boundaries (aborts).\n// See uassert.AbortsWithMessage for `rlm` semantics.\nfunc PanicsWithMessage(t uassert.TestingT, rlm realm, msg string, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.PanicsWithMessage(t, rlm, msg, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// PanicsContains requires that the code inside the specified func panics\n// locally within the same execution realm and the panic message contains the specified substring.\n// See uassert.AbortsWithMessage for `rlm` semantics.\nfunc PanicsContains(t uassert.TestingT, rlm realm, substr string, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.PanicsContains(t, rlm, substr, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// NotPanics requires that the code inside the specified func does NOT panic\n// locally within the same execution realm.\n// Use NotAborts for requiring the absence of panics that cross execution boundaries (aborts).\n// See uassert.AbortsWithMessage for `rlm` semantics.\nfunc NotPanics(t uassert.TestingT, rlm realm, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotPanics(t, rlm, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// Equal requires that two objects are equal.\nfunc Equal(t uassert.TestingT, expected, actual any, msgs ...string) {\n\tt.Helper()\n\tif uassert.Equal(t, expected, actual, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// NotEqual requires that two objects are not equal.\nfunc NotEqual(t uassert.TestingT, expected, actual any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotEqual(t, expected, actual, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// Empty requires that the specified object is empty\n// (zero value, empty string/slice/map, or nil).\nfunc Empty(t uassert.TestingT, obj any, msgs ...string) {\n\tt.Helper()\n\tif uassert.Empty(t, obj, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// NotEmpty requires that the specified object is not empty.\nfunc NotEmpty(t uassert.TestingT, obj any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotEmpty(t, obj, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// Nil requires that the value is nil.\nfunc Nil(t uassert.TestingT, value any, msgs ...string) {\n\tt.Helper()\n\tif uassert.Nil(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// NotNil requires that the value is not nil.\nfunc NotNil(t uassert.TestingT, value any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotNil(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// TypedNil requires that the value is a typed-nil (nil pointer) value.\nfunc TypedNil(t uassert.TestingT, value any, msgs ...string) {\n\tt.Helper()\n\tif uassert.TypedNil(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\n// NotTypedNil requires that the value is not a typed-nil (nil pointer) value.\nfunc NotTypedNil(t uassert.TestingT, value any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotTypedNil(t, value, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"variadic","path":"gno.land/r/tests/vm/variadic","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/variadic\"\ngno = \"0.9\"\n"},{"name":"main.gno","body":"package variadic\n\nimport \"strings\"\n\nfunc Echo(cur realm, vals ...string) string {\n\treturn strings.Join(vals, \" \")\n}\n\nfunc Add(cur realm, nums ...int) int {\n\tres := 0\n\n\tfor _, num := range nums {\n\t\tres += num\n\t}\n\n\treturn res\n}\n\nfunc And(cur realm, booleans ...bool) bool {\n\n\tfor _, boolean := range booleans {\n\t\tif !boolean {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"typeutil","path":"gno.land/p/moul/typeutil","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/moul/typeutil\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"},{"name":"typeutil.gno","body":"// Package typeutil provides utility functions for converting between different types\n// and checking their states. It aims to provide consistent behavior across different\n// types while remaining lightweight and dependency-free.\npackage typeutil\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// stringer is the interface that wraps the String method.\ntype stringer interface {\n\tString() string\n}\n\n// ToString converts any value to its string representation.\n// It supports a wide range of Go types including:\n//   - Basic: string, bool\n//   - Numbers: int, int8-64, uint, uint8-64, float32, float64\n//   - Special: time.Time, address, []byte\n//   - Slices: []T for most basic types\n//   - Maps: map[string]string, map[string]any\n//   - Interface: types implementing String() string\n//\n// Example usage:\n//\n//\tstr := typeutil.ToString(42)               // \"42\"\n//\tstr = typeutil.ToString([]int{1, 2})      // \"[1 2]\"\n//\tstr = typeutil.ToString(map[string]string{ // \"map[a:1 b:2]\"\n//\t    \"a\": \"1\",\n//\t    \"b\": \"2\",\n//\t})\nfunc ToString(val any) string {\n\tif val == nil {\n\t\treturn \"\"\n\t}\n\n\t// First check if value implements Stringer interface\n\tif s, ok := val.(interface{ String() string }); ok {\n\t\treturn s.String()\n\t}\n\n\tswitch v := val.(type) {\n\t// Pointer types - dereference and recurse\n\tcase *string:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn *v\n\tcase *int:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn strconv.Itoa(*v)\n\tcase *bool:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn strconv.FormatBool(*v)\n\tcase *time.Time:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn v.String()\n\tcase *address:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn string(*v)\n\n\t// String types\n\tcase string:\n\t\treturn v\n\tcase stringer:\n\t\treturn v.String()\n\n\t// Special types\n\tcase time.Time:\n\t\treturn v.String()\n\tcase address:\n\t\treturn string(v)\n\tcase []byte:\n\t\treturn string(v)\n\tcase struct{}:\n\t\treturn \"{}\"\n\n\t// Integer types\n\tcase int:\n\t\treturn strconv.Itoa(v)\n\tcase int8:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int16:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int32:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int64:\n\t\treturn strconv.FormatInt(v, 10)\n\tcase uint:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint8:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint16:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint32:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint64:\n\t\treturn strconv.FormatUint(v, 10)\n\n\t// Float types\n\tcase float32:\n\t\treturn strconv.FormatFloat(float64(v), 'f', -1, 32)\n\tcase float64:\n\t\treturn strconv.FormatFloat(v, 'f', -1, 64)\n\n\t// Boolean\n\tcase bool:\n\t\tif v {\n\t\t\treturn \"true\"\n\t\t}\n\t\treturn \"false\"\n\n\t// Slice types\n\tcase []string:\n\t\treturn join(v)\n\tcase []int:\n\t\treturn join(v)\n\tcase []int32:\n\t\treturn join(v)\n\tcase []int64:\n\t\treturn join(v)\n\tcase []float32:\n\t\treturn join(v)\n\tcase []float64:\n\t\treturn join(v)\n\tcase []any:\n\t\treturn join(v)\n\tcase []time.Time:\n\t\treturn joinTimes(v)\n\tcase []stringer:\n\t\treturn join(v)\n\tcase []address:\n\t\treturn joinAddresses(v)\n\tcase [][]byte:\n\t\treturn joinBytes(v)\n\n\t// Map types with various key types\n\tcase map[any]any, map[string]any, map[string]string, map[string]int:\n\t\tvar b strings.Builder\n\t\tb.WriteString(\"map[\")\n\t\tfirst := true\n\n\t\tswitch m := v.(type) {\n\t\tcase map[any]any:\n\t\t\t// Convert all keys to strings for consistent ordering\n\t\t\tkeys := make([]string, 0)\n\t\t\tkeyMap := make(map[string]any)\n\n\t\t\tfor k := range m {\n\t\t\t\tkeyStr := ToString(k)\n\t\t\t\tkeys = append(keys, keyStr)\n\t\t\t\tkeyMap[keyStr] = k\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, keyStr := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\torigKey := keyMap[keyStr]\n\t\t\t\tb.WriteString(keyStr)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(ToString(m[origKey]))\n\t\t\t\tfirst = false\n\t\t\t}\n\n\t\tcase map[string]any:\n\t\t\tkeys := make([]string, 0)\n\t\t\tfor k := range m {\n\t\t\t\tkeys = append(keys, k)\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\tb.WriteString(k)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(ToString(m[k]))\n\t\t\t\tfirst = false\n\t\t\t}\n\n\t\tcase map[string]string:\n\t\t\tkeys := make([]string, 0)\n\t\t\tfor k := range m {\n\t\t\t\tkeys = append(keys, k)\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\tb.WriteString(k)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(m[k])\n\t\t\t\tfirst = false\n\t\t\t}\n\n\t\tcase map[string]int:\n\t\t\tkeys := make([]string, 0)\n\t\t\tfor k := range m {\n\t\t\t\tkeys = append(keys, k)\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\tb.WriteString(k)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(strconv.Itoa(m[k]))\n\t\t\t\tfirst = false\n\t\t\t}\n\t\t}\n\t\tb.WriteString(\"]\")\n\t\treturn b.String()\n\n\t// Default\n\tdefault:\n\t\treturn \"\u003cunknown\u003e\"\n\t}\n}\n\nfunc join(slice any) string {\n\tif IsZero(slice) {\n\t\treturn \"[]\"\n\t}\n\n\titems := ToInterfaceSlice(slice)\n\tif items == nil {\n\t\treturn \"[]\"\n\t}\n\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, item := range items {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(ToString(item))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\nfunc joinTimes(slice []time.Time) string {\n\tif len(slice) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, t := range slice {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(t.String())\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\nfunc joinAddresses(slice []address) string {\n\tif len(slice) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, addr := range slice {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(string(addr))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\nfunc joinBytes(slice [][]byte) string {\n\tif len(slice) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, bytes := range slice {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(string(bytes))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\n// ToBool converts any value to a boolean based on common programming conventions.\n// For example:\n//   - Numbers: 0 is false, any other number is true\n//   - Strings: \"\", \"0\", \"false\", \"f\", \"no\", \"n\", \"off\" are false, others are true\n//   - Slices/Maps: empty is false, non-empty is true\n//   - nil: always false\n//   - bool: direct value\nfunc ToBool(val any) bool {\n\tif IsZero(val) {\n\t\treturn false\n\t}\n\n\t// Handle special string cases\n\tif str, ok := val.(string); ok {\n\t\tstr = strings.ToLower(strings.TrimSpace(str))\n\t\treturn str != \"\" \u0026\u0026 str != \"0\" \u0026\u0026 str != \"false\" \u0026\u0026 str != \"f\" \u0026\u0026 str != \"no\" \u0026\u0026 str != \"n\" \u0026\u0026 str != \"off\"\n\t}\n\n\treturn true\n}\n\n// IsZero returns true if the value represents a \"zero\" or \"empty\" state for its type.\n// For example:\n//   - Numbers: 0\n//   - Strings: \"\"\n//   - Slices/Maps: empty\n//   - nil: true\n//   - bool: false\n//   - time.Time: IsZero()\n//   - address: empty string\nfunc IsZero(val any) bool {\n\tif val == nil {\n\t\treturn true\n\t}\n\n\tswitch v := val.(type) {\n\t// Pointer types - nil pointer is zero, otherwise check pointed value\n\tcase *bool:\n\t\treturn v == nil || !*v\n\tcase *string:\n\t\treturn v == nil || *v == \"\"\n\tcase *int:\n\t\treturn v == nil || *v == 0\n\tcase *time.Time:\n\t\treturn v == nil || v.IsZero()\n\tcase *address:\n\t\treturn v == nil || string(*v) == \"\"\n\n\t// Bool\n\tcase bool:\n\t\treturn !v\n\n\t// String types\n\tcase string:\n\t\treturn v == \"\"\n\tcase stringer:\n\t\treturn v.String() == \"\"\n\n\t// Integer types\n\tcase int:\n\t\treturn v == 0\n\tcase int8:\n\t\treturn v == 0\n\tcase int16:\n\t\treturn v == 0\n\tcase int32:\n\t\treturn v == 0\n\tcase int64:\n\t\treturn v == 0\n\tcase uint:\n\t\treturn v == 0\n\tcase uint8:\n\t\treturn v == 0\n\tcase uint16:\n\t\treturn v == 0\n\tcase uint32:\n\t\treturn v == 0\n\tcase uint64:\n\t\treturn v == 0\n\n\t// Float types\n\tcase float32:\n\t\treturn v == 0\n\tcase float64:\n\t\treturn v == 0\n\n\t// Special types\n\tcase []byte:\n\t\treturn len(v) == 0\n\tcase time.Time:\n\t\treturn v.IsZero()\n\tcase address:\n\t\treturn string(v) == \"\"\n\n\t// Slices (check if empty)\n\tcase []string:\n\t\treturn len(v) == 0\n\tcase []int:\n\t\treturn len(v) == 0\n\tcase []int32:\n\t\treturn len(v) == 0\n\tcase []int64:\n\t\treturn len(v) == 0\n\tcase []float32:\n\t\treturn len(v) == 0\n\tcase []float64:\n\t\treturn len(v) == 0\n\tcase []any:\n\t\treturn len(v) == 0\n\tcase []time.Time:\n\t\treturn len(v) == 0\n\tcase []address:\n\t\treturn len(v) == 0\n\tcase [][]byte:\n\t\treturn len(v) == 0\n\tcase []stringer:\n\t\treturn len(v) == 0\n\n\t// Maps (check if empty)\n\tcase map[string]string:\n\t\treturn len(v) == 0\n\tcase map[string]any:\n\t\treturn len(v) == 0\n\n\tdefault:\n\t\treturn false // non-nil unknown types are considered non-zero\n\t}\n}\n\n// ToInterfaceSlice converts various slice types to []any\nfunc ToInterfaceSlice(val any) []any {\n\tswitch v := val.(type) {\n\tcase []any:\n\t\treturn v\n\tcase []string:\n\t\tresult := make([]any, len(v))\n\t\tfor i, s := range v {\n\t\t\tresult[i] = s\n\t\t}\n\t\treturn result\n\tcase []int:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []int32:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []int64:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []float32:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []float64:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []bool:\n\t\tresult := make([]any, len(v))\n\t\tfor i, b := range v {\n\t\t\tresult[i] = b\n\t\t}\n\t\treturn result\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// ToMapStringInterface converts a map with string keys and any value type to map[string]any\nfunc ToMapStringInterface(m any) (map[string]any, error) {\n\tresult := make(map[string]any)\n\n\tswitch v := m.(type) {\n\tcase map[string]any:\n\t\treturn v, nil\n\tcase map[string]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]int64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]float64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]bool:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string][]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[string][]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[string][]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]map[string]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]map[string]string:\n\t\tfor k, val := range v {\n\t\t\tif converted, err := ToMapStringInterface(val); err == nil {\n\t\t\t\tresult[k] = converted\n\t\t\t} else {\n\t\t\t\treturn nil, errors.New(\"failed to convert nested map at key: \" + k)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported map type: \" + ToString(m))\n\t}\n\n\treturn result, nil\n}\n\n// ToMapIntInterface converts a map with int keys and any value type to map[int]any\nfunc ToMapIntInterface(m any) (map[int]any, error) {\n\tresult := make(map[int]any)\n\n\tswitch v := m.(type) {\n\tcase map[int]any:\n\t\treturn v, nil\n\tcase map[int]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]int64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]float64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]bool:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int][]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[int][]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[int][]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]map[string]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]map[int]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported map type: \" + ToString(m))\n\t}\n\n\treturn result, nil\n}\n\n// ToStringSlice converts various slice types to []string\nfunc ToStringSlice(val any) []string {\n\tswitch v := val.(type) {\n\tcase []string:\n\t\treturn v\n\tcase []any:\n\t\tresult := make([]string, len(v))\n\t\tfor i, item := range v {\n\t\t\tresult[i] = ToString(item)\n\t\t}\n\t\treturn result\n\tcase []int:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.Itoa(n)\n\t\t}\n\t\treturn result\n\tcase []int32:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatInt(int64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []int64:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatInt(n, 10)\n\t\t}\n\t\treturn result\n\tcase []float32:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatFloat(float64(n), 'f', -1, 32)\n\t\t}\n\t\treturn result\n\tcase []float64:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatFloat(n, 'f', -1, 64)\n\t\t}\n\t\treturn result\n\tcase []bool:\n\t\tresult := make([]string, len(v))\n\t\tfor i, b := range v {\n\t\t\tresult[i] = strconv.FormatBool(b)\n\t\t}\n\t\treturn result\n\tcase []time.Time:\n\t\tresult := make([]string, len(v))\n\t\tfor i, t := range v {\n\t\t\tresult[i] = t.String()\n\t\t}\n\t\treturn result\n\tcase []address:\n\t\tresult := make([]string, len(v))\n\t\tfor i, addr := range v {\n\t\t\tresult[i] = string(addr)\n\t\t}\n\t\treturn result\n\tcase [][]byte:\n\t\tresult := make([]string, len(v))\n\t\tfor i, b := range v {\n\t\t\tresult[i] = string(b)\n\t\t}\n\t\treturn result\n\tcase []stringer:\n\t\tresult := make([]string, len(v))\n\t\tfor i, s := range v {\n\t\t\tresult[i] = s.String()\n\t\t}\n\t\treturn result\n\tcase []uint:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint8:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint16:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint32:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint64:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(n, 10)\n\t\t}\n\t\treturn result\n\tdefault:\n\t\t// Try to convert using reflection if it's a slice\n\t\tif slice := ToInterfaceSlice(val); slice != nil {\n\t\t\tresult := make([]string, len(slice))\n\t\t\tfor i, item := range slice {\n\t\t\t\tresult[i] = ToString(item)\n\t\t\t}\n\t\t\treturn result\n\t\t}\n\t\treturn nil\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"allowancesender","path":"gno.land/p/jaekwon/allowancesender/v0","files":[{"name":"allowancesender.gno","body":"// Package allowancesender provides a bounded, granter-revocable\n// spending capability that wraps a banker.Banker source.\n//\n// # Use case\n//\n// Realm A wants to grant realm B the ability to spend up to some\n// bounded amount from A's address (or A's tx envelope) within a single\n// call, without giving B unbounded access. After A's call to B returns,\n// A revokes the capability via Close(); B cannot use it across tx\n// boundaries even if it persisted the reference.\n//\n// # Canonical pattern\n//\n//\tsrc := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n//\tal  := allowancesender.New(src, granterAddr,\n//\t           chain.NewCoins(chain.NewCoin(\"ugnot\", 1_000_000)))\n//\tdefer al.Close()\n//\tbRealm.DoSomething(cross, al)  // B can call al.Send up to cap\n//\n// On return (success or panic), `defer al.Close()` flips the closed\n// flag. If B persisted al, B's stored reference is now dead — any\n// later al.Send() panics with \"closed\".\n//\n// # Naming note\n//\n// Despite the underlying type wrapping a banker.Banker, *AllowanceSender\n// does NOT satisfy the banker.Banker interface. The other Banker methods\n// (GetCoins, TotalCoin, IssueCoin, RemoveCoin) have no meaningful\n// behavior on an allowance abstraction:\n//\n//   - GetCoins would either leak the granter realm's full balance\n//     (misleading) or return Remaining() (lying about the addr arg).\n//   - TotalCoin/IssueCoin/RemoveCoin are out of scope.\n//\n// Send is the only meaningful operation; hence the \"Sender\" name. If\n// you need to plug into a banker.Banker-shaped API, write an adapter\n// in your own package.\n//\n// # Security model\n//\n//   - Pointer-based API. All callees that hold a *AllowanceSender share\n//     the same underlying state. Close() flips a bool that every reader\n//     observes synchronously.\n//   - Inner banker is an unexported field. Callees cannot extract it\n//     to bypass the wrapper. Language-enforced.\n//   - Cap is enforced per-denom via chain.Coins.AmountOf. Any denom in\n//     amt that is missing from cap implicitly has capAmt=0; positive\n//     spend on such a denom panics with \"cap exceeded\".\n//   - Persistence is the kill switch's friend, not its enemy. The\n//     closed flag persists with the struct, so a callee that stored\n//     the reference for cross-tx use sees a closed allowance.\n//   - Defer-close survives panic. If B's call panics, the tx reverts\n//     entirely (so the close is irrelevant — state reverts anyway). On\n//     success, defer fires before granter's function returns,\n//     committing closed=true with the rest of the tx state.\n//   - Cap/Spent return defensive copies. Mutating the returned\n//     chain.Coins does not affect internal state.\n//   - Inner banker panic on SendCoins (e.g. bank-keeper \"insufficient\n//     funds\") rolls back the spent counter — caller's accounting stays\n//     accurate even if a defer-recover swallows the panic. (Without\n//     recovery, the entire tx reverts and the rollback is moot.)\n//   - Arithmetic uses math/overflow.Add64. Overflow on (spent+amount)\n//     panics rather than silently wrapping (which could bypass the cap\n//     check). Negative amounts are explicitly rejected.\n//\n// # Design choices\n//\n//   - Pointer type, not value: forces shared state across references.\n//     Value-copy AllowanceSender would have separate closed flags and\n//     defeat the kill switch.\n//   - One-shot Close, not graduated: simpler invariant. If you need\n//     \"resume later,\" create a fresh allowance.\n//   - No destination allowlist: out of scope. If the granter wants to\n//     restrict where funds go, wrap this further.\n//   - No time-based expiry: rely on Close. Block-height stamping or\n//     deadline checks would require runtime cooperation; this package\n//     is pure Gno.\n//   - Cap is multi-denom (chain.Coins, not int64): supports\n//     heterogeneous payment policies. Most callers use single-denom.\n//   - Idempotent Close: safe to defer-Close even if Close was called\n//     manually earlier in the function.\n//   - Does NOT satisfy banker.Banker: see \"Naming note\" above.\n//\n// # Limitations\n//\n//   - Granter retains direct access to the underlying source banker.\n//     The AllowanceSender only constrains the wrapped capability, not\n//     the granter's ability to spend its own funds via other means.\n//   - No cross-realm enforcement of granter identity. The source\n//     banker's own pkgAddr check (banker.gno) rejects mismatched\n//     froms. This package relies on banker's own protection there.\n//   - Re-entrancy: nested allowances work (each is independent), but\n//     do NOT re-use the same AllowanceSender pointer across grants —\n//     always create a fresh one. Re-using would mix spent counters.\n//   - Cap with duplicate denoms (constructed by hand, not via\n//     chain.NewCoins) will trigger chain.Coins.AmountOf to panic on\n//     read. Use chain.NewCoins(...) to construct cap; it deduplicates.\n//   - Persistence: granter is responsible for clearing references to\n//     closed AllowanceSender pointers from its own state if it wants\n//     them garbage-collected; otherwise they linger as dead structs.\n//\n// # What this package does NOT solve\n//\n//   - \"Allowance survives across N txs but not N+1 txs.\" Use a session\n//     counter pattern in the granter realm; this package's Close is\n//     binary, not deadline-based.\n//   - \"Fungible-token (grc20) allowances.\" Use the grc20 package's\n//     own Approve/Allowance/TransferFrom triple. AllowanceSender is\n//     for native (chain-coin) sends only.\n//\n// # Events\n//\n// AllowanceSender emits chain events at two points so off-chain\n// observers can track allowance lifecycles. Events are emitted from\n// the calling realm's package path (whichever realm holds the\n// AllowanceSender pointer when the method is called).\n//\n//   - \"AllowanceSenderSend\": on each successful Send, with attributes\n//     \"payer\", \"to\", \"amount\", \"spent_total\", \"remaining\".\n//   - \"AllowanceSenderClose\": on Close (only the first call; idempotent\n//     re-Closes do not re-emit).\n//\n// The underlying inner banker also emits its own bank-keeper events\n// for the actual coin movement; AllowanceSender's events sit on top of\n// those for allowance-level audit trails.\npackage allowancesender\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"math/overflow\"\n)\n\n// Event names emitted by AllowanceSender. Use these constants when\n// asserting on events from off-chain observers or test code.\nconst (\n\tEventSend  = \"AllowanceSenderSend\"\n\tEventClose = \"AllowanceSenderClose\"\n)\n\n// AllowanceSender is a bounded, revocable spending capability over a\n// source banker.Banker. Always pass *AllowanceSender (pointer); value\n// copies are not supported and would not share state.\n//\n// Does NOT satisfy banker.Banker — the other Banker methods (GetCoins,\n// TotalCoin, IssueCoin, RemoveCoin) have no meaningful behavior on an\n// allowance and are deliberately omitted.\ntype AllowanceSender struct {\n\tinner  banker.Banker // unexported — callees cannot extract\n\tpayer  address       // address debited; must match inner's source\n\tlimit  chain.Coins   // maximum cumulative spend, per-denom\n\tspent  chain.Coins   // running total of spends, per-denom\n\tclosed bool          // once true, all further Send calls panic\n}\n\n// New creates a new AllowanceSender wrapping inner with the given limit.\n// payer is the address that inner.SendCoins will draw from (must match\n// the realm address inner was created against; the banker enforces this\n// at SendCoins time).\n//\n// inner must be the canonical Banker produced by banker.NewBanker;\n// hand-rolled Banker implementations (no-op fakes, decorators) are\n// rejected via banker.IsCanonical. This guarantees that a callee\n// receiving a *AllowanceSender from a granter realm can rely on Send\n// actually moving real coins via the bank-keeper, not via a fake\n// banker that no-ops SendCoins.\n//\n// Panics if inner is not canonical or payer is invalid. limit may be\n// empty (zero allowance — every Send panics with \"cap exceeded\");\n// limit may also contain multiple denoms.\nfunc New(inner banker.Banker, payer address, limit chain.Coins) *AllowanceSender {\n\tif !banker.IsCanonical(inner) {\n\t\tpanic(\"allowancesender: inner banker is not the canonical chain/banker.Banker\")\n\t}\n\tif !payer.IsValid() {\n\t\tpanic(\"allowancesender: payer address is invalid\")\n\t}\n\treturn \u0026AllowanceSender{\n\t\tinner: inner,\n\t\tpayer: payer,\n\t\tlimit: limit,\n\t}\n}\n\n// Send debits up to (cap - spent) per denom from payer to to via the\n// inner banker. Updates spent on success. On any panic (including\n// inner.SendCoins panic for bank-level reasons like insufficient\n// balance), spent is rolled back — caller's accounting stays accurate\n// even if a caller wraps Send in a defer-recover.\n//\n// Arithmetic uses math/overflow.Add64; an overflow on (spent+amount)\n// panics with a distinct message rather than silently wrapping. This\n// closes the int64-overflow vector where a malicious callee passes a\n// MaxInt64-style amount to bypass the cap check.\n//\n// Negative amounts are rejected explicitly. chain.NewCoin permits\n// signed amounts at construction; we don't accept them.\n//\n// Emits \"AllowanceSenderSend\" event on success.\n//\n// Panics:\n//   - \"closed\" if Close has been called.\n//   - \"negative amount not allowed\" if any c.Amount \u003c 0.\n//   - \"amount overflow on spent+amt\" if int64 addition would overflow.\n//   - \"cap exceeded for denom \u003cdenom\u003e\" if any denom in amt would push\n//     spent over cap.\n//   - inner.SendCoins panics propagate (typically \"insufficient\n//     funds\" from the bank keeper); spent is rolled back first.\nfunc (a *AllowanceSender) Send(to address, amt chain.Coins) {\n\tif a.closed {\n\t\tpanic(\"allowancesender: closed\")\n\t}\n\n\t// Per-denom validation. Done in a separate pass before any state\n\t// mutation so a partial failure doesn't leak into spent.\n\tfor _, c := range amt {\n\t\tif c.Amount \u003c 0 {\n\t\t\tpanic(\"allowancesender: negative amount not allowed for denom \" + c.Denom)\n\t\t}\n\t\tcapAmt := a.limit.AmountOf(c.Denom)\n\t\tspentAmt := a.spent.AmountOf(c.Denom)\n\t\tsum, ok := overflow.Add64(spentAmt, c.Amount)\n\t\tif !ok {\n\t\t\tpanic(\"allowancesender: amount overflow on spent+amt for denom \" + c.Denom)\n\t\t}\n\t\tif sum \u003e capAmt {\n\t\t\tpanic(\"allowancesender: cap exceeded for denom \" + c.Denom)\n\t\t}\n\t}\n\n\t// Snapshot for rollback. If inner.SendCoins panics — even if a\n\t// caller's defer-recover swallows the panic — the deferred\n\t// rollback below restores spent before re-raising. Without this,\n\t// a recovered panic would leave spent inflated for a transfer\n\t// that didn't move funds at the bank-keeper level, allowing an\n\t// adversary to \"burn\" allowance against failed sends.\n\tprevSpent := a.spent\n\ta.spent = a.spent.Add(amt)\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\ta.spent = prevSpent\n\t\t\tpanic(r) // re-raise so caller learns of the failure\n\t\t}\n\t}()\n\n\ta.inner.SendCoins(a.payer, to, amt)\n\n\t// Reached only on successful inner.SendCoins. Emit the audit\n\t// event from the granter realm's perspective.\n\tchain.Emit(EventSend,\n\t\t\"payer\", a.payer.String(),\n\t\t\"to\", to.String(),\n\t\t\"amount\", amt.String(),\n\t\t\"spent_total\", a.spent.String(),\n\t\t\"remaining\", a.Remaining().String(),\n\t)\n}\n\n// Cap returns a defensive copy of the maximum cumulative spend.\n// Mutating the returned slice does not affect internal state.\nfunc (a *AllowanceSender) Cap() chain.Coins {\n\treturn cloneCoins(a.limit)\n}\n\n// Spent returns a defensive copy of the cumulative amount spent.\n// Mutating the returned slice does not affect internal state.\nfunc (a *AllowanceSender) Spent() chain.Coins {\n\treturn cloneCoins(a.spent)\n}\n\n// Remaining returns cap - spent, per denom. Denoms with non-positive\n// remainder are omitted.\nfunc (a *AllowanceSender) Remaining() chain.Coins {\n\tif len(a.limit) == 0 {\n\t\treturn nil\n\t}\n\tout := make(chain.Coins, 0, len(a.limit))\n\tfor _, c := range a.limit {\n\t\trem := c.Amount - a.spent.AmountOf(c.Denom)\n\t\tif rem \u003e 0 {\n\t\t\tout = append(out, chain.NewCoin(c.Denom, rem))\n\t\t}\n\t}\n\treturn out\n}\n\n// Closed reports whether Close has been called.\nfunc (a *AllowanceSender) Closed() bool {\n\treturn a.closed\n}\n\n// Close terminates the allowance. Subsequent Send calls panic.\n// Idempotent — calling twice is fine; the second call is a no-op and\n// does not re-emit the AllowanceSenderClose event.\nfunc (a *AllowanceSender) Close() {\n\tif a.closed {\n\t\treturn\n\t}\n\ta.closed = true\n\tchain.Emit(EventClose,\n\t\t\"payer\", a.payer.String(),\n\t\t\"spent_total\", a.spent.String(),\n\t)\n}\n\n// Payer returns the address that this allowance debits from.\nfunc (a *AllowanceSender) Payer() address {\n\treturn a.payer\n}\n\n// cloneCoins returns a fresh chain.Coins that does not share an\n// underlying array with the source. Used to defend internal state\n// from caller mutation.\nfunc cloneCoins(src chain.Coins) chain.Coins {\n\tif len(src) == 0 {\n\t\treturn nil\n\t}\n\tout := make(chain.Coins, len(src))\n\tcopy(out, src)\n\treturn out\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/jaekwon/allowancesender/v0\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"wugnot","path":"gno.land/r/gnoland/wugnot","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gnoland/wugnot\"\ngno = \"0.9\"\n"},{"name":"wugnot.gno","body":"package wugnot\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nvar (\n\tToken *grc20.Token\n\tadm   *grc20.PrivateLedger\n)\n\nconst (\n\tugnotMinDeposit  int64 = 1000\n\twugnotMinDeposit int64 = 1\n)\n\nfunc init(cur realm) {\n\t// wugnot only ever creates this one token, so id 0 can't collide.\n\tToken, adm = grc20.NewToken(\"wrapped GNOT\", \"wugnot\", 0, 0, cur)\n\tgrc20reg.Register(cross(cur), Token, \"\")\n}\n\nfunc Deposit(cur realm) {\n\t// Prevent cross-realm MITM: without this, an intermediary could\n\t// deposit on behalf of the caller and mint wugnot to itself\n\t// instead of the actual sender.\n\truntime.AssertOriginCall()\n\tcaller := cur.Previous().Address()\n\tsent := unsafe.OriginSend()\n\tamount := sent.AmountOf(\"ugnot\")\n\n\trequire(int64(amount) \u003e= ugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d ugnot.\", amount, ugnotMinDeposit))\n\n\tcheckErr(adm.Mint(caller, int64(amount)))\n}\n\nfunc Withdraw(cur realm, amount int64) {\n\truntime.AssertOriginCall()\n\trequire(amount \u003e= wugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d wugnot.\", amount, wugnotMinDeposit))\n\n\tcaller := cur.Previous().Address()\n\tpkgaddr := cur.Address()\n\tcallerBal := Token.BalanceOf(caller)\n\trequire(amount \u003c= callerBal, ufmt.Sprintf(\"Insufficient balance: %d available, %d needed.\", callerBal, amount))\n\n\t// send swapped ugnots to qcaller\n\tstdBanker := banker.NewBanker(banker.BankerTypeRealmSend, cur)\n\tsend := chain.Coins{{\"ugnot\", int64(amount)}}\n\tstdBanker.SendCoins(pkgaddr, caller, send)\n\tcheckErr(adm.Burn(caller, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\"\n\t}\n}\n\nfunc TotalSupply() int64 {\n\treturn Token.TotalSupply()\n}\n\nfunc BalanceOf(owner address) int64 {\n\treturn Token.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender address) int64 {\n\treturn Token.Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, to address, amount int64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.Transfer(0, cur, to, amount))\n}\n\nfunc Approve(cur realm, spender address, amount int64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.Approve(0, cur, spender, amount))\n}\n\nfunc TransferFrom(cur realm, from, to address, amount int64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.TransferFrom(0, cur, from, to, amount))\n}\n\nfunc require(condition bool, msg string) {\n\tif !condition {\n\t\tpanic(msg)\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"proposal","path":"gno.land/r/gnops/valopers/proposal","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gnops/valopers/proposal\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"},{"name":"proposal.gno","body":"package proposal\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\tvalopers \"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gov/dao\"\n\tsysparams \"gno.land/r/sys/params\"\n\tvalidators \"gno.land/r/sys/validators/v3\"\n)\n\nvar (\n\tErrValidatorMissing = errors.New(\"the validator is missing\")\n\tErrSameValues       = errors.New(\"the valoper has the same voting power and pubkey\")\n)\n\n// NewValidatorProposalRequest creates a proposal request to the GovDAO\n// for adding (or removing) the given valoper to/from the validator set.\n//\n// Signature is preserved for historical-replay compatibility (gnoland-1\n// callers call this with a single address). Body is rewired to call\n// v3's operator-keyed NewValidatorProposalRequest with a single-element\n// slice; v3's executor re-resolves the signing pubkey from valoperCache\n// at execution time, so a mid-flight rotation publishes the current key.\nfunc NewValidatorProposalRequest(cur realm, addr address) dao.ProposalRequest {\n\tvar (\n\t\tvaloper     = valopers.GetByAddr(addr)\n\t\tvotingPower = uint64(1)\n\t)\n\n\texist := validators.IsValidator(valoper.SigningAddress)\n\n\t// Determine the voting power\n\tif !valoper.KeepRunning {\n\t\tif !exist {\n\t\t\tpanic(ErrValidatorMissing)\n\t\t}\n\t\tvotingPower = uint64(0)\n\t}\n\n\tif exist {\n\t\tvalidator := validators.GetValidator(valoper.SigningAddress)\n\t\tif validator.VotingPower == votingPower \u0026\u0026 validator.PubKey == valoper.SigningPubKey {\n\t\t\tpanic(ErrSameValues)\n\t\t}\n\t}\n\n\t// Craft the proposal title and description, framed around the\n\t// valoper profile. Voters see the operator identity (moniker +\n\t// operator address); the signing key is an implementation detail\n\t// resolved at execution time by v3's executor.\n\ttitle := ufmt.Sprintf(\n\t\t\"Add valoper %s to the valset\",\n\t\tvaloper.Moniker,\n\t)\n\n\tdescription := ufmt.Sprintf(\"Valoper profile: [%s](/r/gnops/valopers:%s)\\n\\n%s\",\n\t\tvaloper.Moniker,\n\t\tvaloper.OperatorAddress,\n\t\tvaloper.Render(),\n\t)\n\n\treturn validators.NewValidatorProposalRequest(cross(cur),\n\t\t[]validators.ValoperChange{validators.NewValoperChange(valoper.OperatorAddress, votingPower)},\n\t\ttitle,\n\t\tdescription,\n\t)\n}\n\n// ProposeNewInstructionsProposalRequest creates a proposal to the GovDAO\n// for updating the realm instructions.\nfunc ProposeNewInstructionsProposalRequest(cur realm, newInstructions string) dao.ProposalRequest {\n\tcb := valopers.NewInstructionsProposalCallback(newInstructions)\n\t// Create a proposal\n\ttitle := \"/p/gnops/valopers: Update instructions\"\n\tdescription := ufmt.Sprintf(\"Update the instructions to: \\n\\n%s\", newInstructions)\n\n\te := dao.NewSimpleExecutor(0, cur, cb, \"\")\n\n\treturn dao.NewProposalRequest(title, description, e)\n}\n\n// ProposeNewMinFeeProposalRequest creates a proposal to the GovDAO\n// for updating the minimum fee to register a new valoper. Signature\n// preserved for historical-replay compatibility (gnoland-1's\n// set_minfee.gno MsgRun calls this); body now delegates to the\n// generic sys/params factory so the fee lives in\n// node:valoper:register_fee.\nfunc ProposeNewMinFeeProposalRequest(cur realm, newMinFee int64) dao.ProposalRequest {\n\treturn sysparams.NewSysParamUint64PropRequest(cross(cur), \"node\", \"valoper\", \"register_fee\", uint64(newMinFee))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"init","path":"gno.land/r/sys/users/init","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/users/init\"\ngno = \"0.9\"\n"},{"name":"init.gno","body":"// Package init provides basic user registration.\n//\n// SECURITY: every public function in this package is genesis-only. The\n// realm exists to seed initial users into r/sys/users at chain genesis\n// and to register itself as a controller. After genesis (block height\n// \u003e 0), no caller may use these functions.\n//\n// Without the genesis-only gate, the wide-open `RegisterUser` wrapper\n// would let any EOA land-grab any name (including reserved-sounding\n// names like \"administrator\" or \"vitalik\") to any address — for free,\n// without the namereg/v1 payment, blacklist, or canonical-collision\n// checks. The gate closes that bypass.\n//\n// Post-genesis user registration must go through a whitelisted\n// controller that enforces its own policy (e.g. r/sys/namereg/v1).\npackage init\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// Bootstrap registers this package as a controller in r/sys/users.\n// Genesis-only via AddControllerAtGenesis's own height==0 gate.\nfunc Bootstrap(cur realm) {\n\tusers.AddControllerAtGenesis(cross(cur), chain.PackageAddress(\"gno.land/r/sys/users/init\"))\n}\n\n// RegisterUser registers a new user in r/sys/users at chain genesis.\n// PANICS if called after genesis (height \u003e 0).\n//\n// Uses RegisterUserIgnoreCanonical: the genesis seed set is curated, and\n// any deliberate confusable reservations (e.g. registering both `vitalik`\n// and `vital1k` to two different addresses) must not abort chain bring-\n// up. Decision #14 (later-wins) applies, so order in genesis_txs.jsonl\n// determines which name owns the canonical pointer when stems collide.\nfunc RegisterUser(cur realm, name string, addr address) {\n\tif runtime.ChainHeight() != 0 {\n\t\tpanic(\"r/sys/users/init.RegisterUser: genesis-only; use a whitelisted controller post-genesis (e.g. r/sys/namereg/v1.Register)\")\n\t}\n\tif err := users.RegisterUserIgnoreCanonical(cross(cur), name, addr); err != nil {\n\t\tpanic(err)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"json","path":"gno.land/p/onbloc/json","files":[{"name":"LICENSE","body":"# MIT License\n\nCopyright (c) 2019 Pyzhov Stepan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"},{"name":"README.md","body":"# JSON Parser\n\nThe JSON parser is a package that provides functionality for parsing and processing JSON strings. This package accepts JSON strings as byte slices.\n\nCurrently, gno does not [support the `reflect` package](https://docs.gno.land/resources/effective-gno#reflection-is-never-clear), so it cannot retrieve type information at runtime. Therefore, it is designed to infer and handle type information when parsing JSON strings using a state machine approach.\n\nAfter passing through the state machine, JSON strings are represented as the `Node` type. The `Node` type represents nodes for JSON data, including various types such as `ObjectNode`, `ArrayNode`, `StringNode`, `NumberNode`, `BoolNode`, and `NullNode`.\n\nThis package provides methods for manipulating, searching, and extracting the Node type.\n\n## State Machine\n\nTo parse JSON strings, a [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine) approach is used. The state machine transitions to the next state based on the current state and the input character while parsing the JSON string. Through this method, type information can be inferred and processed without reflect, and the amount of parser code can be significantly reduced.\n\nThe image below shows the state transitions of the state machine according to the states and input characters.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e __: Start\n    __ --\u003e ST: String\n    __ --\u003e MI: Number\n    __ --\u003e ZE: Zero\n    __ --\u003e IN: Integer\n    __ --\u003e T1: Boolean (true)\n    __ --\u003e F1: Boolean (false)\n    __ --\u003e N1: Null\n    __ --\u003e ec: Empty Object End\n    __ --\u003e cc: Object End\n    __ --\u003e bc: Array End\n    __ --\u003e co: Object Begin\n    __ --\u003e bo: Array Begin\n    __ --\u003e cm: Comma\n    __ --\u003e cl: Colon\n    __ --\u003e OK: Success/End\n    ST --\u003e OK: String Complete\n    MI --\u003e OK: Number Complete\n    ZE --\u003e OK: Zero Complete\n    IN --\u003e OK: Integer Complete\n    T1 --\u003e OK: True Complete\n    F1 --\u003e OK: False Complete\n    N1 --\u003e OK: Null Complete\n    ec --\u003e OK: Empty Object Complete\n    cc --\u003e OK: Object Complete\n    bc --\u003e OK: Array Complete\n    co --\u003e OB: Inside Object\n    bo --\u003e AR: Inside Array\n    cm --\u003e KE: Expecting New Key\n    cm --\u003e VA: Expecting New Value\n    cl --\u003e VA: Expecting Value\n    OB --\u003e ST: String in Object (Key)\n    OB --\u003e ec: Empty Object\n    OB --\u003e cc: End Object\n    AR --\u003e ST: String in Array\n    AR --\u003e bc: End Array\n    KE --\u003e ST: String as Key\n    VA --\u003e ST: String as Value\n    VA --\u003e MI: Number as Value\n    VA --\u003e T1: True as Value\n    VA --\u003e F1: False as Value\n    VA --\u003e N1: Null as Value\n    OK --\u003e [*]: End\n```\n\n## Examples\n\nThis package provides parsing functionality along with encoding and decoding functionality. The following examples demonstrate how to use this package.\n\n### Decoding\n\nDecoding (or Unmarshaling) is the functionality that converts an input byte slice JSON string into a `Node` type.\n\nThe converted `Node` type allows you to modify the JSON data or search and extract data that meets specific conditions.\n\n```go\npackage main\n\nimport (\n    \"gno.land/p/demo/json\"\n    \"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc main() {\n    node, err := json.Unmarshal([]byte(`{\"foo\": \"var\"}`))\n    if err != nil {\n        ufmt.Errorf(\"error: %v\", err)\n    }\n\n    ufmt.Sprintf(\"node: %v\", node)\n}\n```\n\n### Encoding\n\nEncoding (or Marshaling) is the functionality that converts JSON data represented as a Node type into a byte slice JSON string.\n\n\u003e ⚠️ Caution: Converting a large `Node` type into a JSON string may _impact performance_. or might be cause _unexpected behavior_.\n\n```go\npackage main\n\nimport (\n    \"gno.land/p/demo/json\"\n    \"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc main() {\n    node := ObjectNode(\"\", map[string]*Node{\n        \"foo\": StringNode(\"foo\", \"bar\"),\n        \"baz\": NumberNode(\"baz\", 100500),\n        \"qux\": NullNode(\"qux\"),\n    })\n\n    b, err := json.Marshal(node)\n    if err != nil {\n        ufmt.Errorf(\"error: %v\", err)\n    }\n\n    ufmt.Sprintf(\"json: %s\", string(b))\n}\n```\n\n### Searching\n\nOnce the JSON data converted into a `Node` type, you can **search** and **extract** data that satisfy specific conditions. For example, you can find data with a specific type or data with a specific key.\n\nTo use this functionality, you can use methods in the `GetXXX` prefixed methods. The `MustXXX` methods also provide the same functionality as the former methods, but they will **panic** if data doesn't satisfies the condition.\n\nHere is an example of finding data with a specific key. For more examples, please refer to the [node.gno](node.gno) file.\n\n```go\npackage main\n\nimport (\n    \"gno.land/p/demo/json\"\n    \"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc main() {\n    root, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n    if err != nil {\n        ufmt.Errorf(\"error: %v\", err)\n    }\n\n    value, err := root.GetKey(\"foo\")\n    if err != nil {\n        ufmt.Errorf(\"error occurred while getting key, %s\", err)\n    }\n\n    if value.MustBool() != true {\n        ufmt.Errorf(\"value is not true\")\n    }\n\n    value, err = root.GetKey(\"bar\")\n    if err != nil {\n        t.Errorf(\"error occurred while getting key, %s\", err)\n    }\n\n    _, err = root.GetKey(\"baz\")\n    if err == nil {\n        t.Errorf(\"key baz is not exist. must be failed\")\n    }\n}\n```\n\n## Contributing\n\nPlease submit any issues or pull requests for this package through the GitHub repository at [gnolang/gno](\u003chttps://github.com/gnolang/gno\u003e).\n"},{"name":"buffer.gno","body":"package json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype buffer struct {\n\tdata   []byte\n\tlength int\n\tindex  int\n\n\tlast  States\n\tstate States\n\tclass Classes\n}\n\n// newBuffer creates a new buffer with the given data\nfunc newBuffer(data []byte) *buffer {\n\treturn \u0026buffer{\n\t\tdata:   data,\n\t\tlength: len(data),\n\t\tlast:   GO,\n\t\tstate:  GO,\n\t}\n}\n\n// first retrieves the first non-whitespace (or other escaped) character in the buffer.\nfunc (b *buffer) first() (byte, error) {\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tc := b.data[b.index]\n\n\t\tif !(c == whiteSpace || c == carriageReturn || c == newLine || c == tab) {\n\t\t\treturn c, nil\n\t\t}\n\t}\n\n\treturn 0, io.EOF\n}\n\n// current returns the byte of the current index.\nfunc (b *buffer) current() (byte, error) {\n\tif b.index \u003e= b.length {\n\t\treturn 0, io.EOF\n\t}\n\n\treturn b.data[b.index], nil\n}\n\n// next moves to the next byte and returns it.\nfunc (b *buffer) next() (byte, error) {\n\tb.index++\n\treturn b.current()\n}\n\n// step just moves to the next position.\nfunc (b *buffer) step() error {\n\t_, err := b.next()\n\treturn err\n}\n\n// move moves the index by the given position.\nfunc (b *buffer) move(pos int) error {\n\tnewIndex := b.index + pos\n\n\tif newIndex \u003e b.length {\n\t\treturn io.EOF\n\t}\n\n\tb.index = newIndex\n\n\treturn nil\n}\n\n// slice returns the slice from the current index to the given position.\nfunc (b *buffer) slice(pos int) ([]byte, error) {\n\tend := b.index + pos\n\n\tif end \u003e b.length {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn b.data[b.index:end], nil\n}\n\n// sliceFromIndices returns a slice of the buffer's data starting from 'start' up to (but not including) 'stop'.\nfunc (b *buffer) sliceFromIndices(start, stop int) []byte {\n\tif start \u003e b.length {\n\t\tstart = b.length\n\t}\n\n\tif stop \u003e b.length {\n\t\tstop = b.length\n\t}\n\n\treturn b.data[start:stop]\n}\n\n// skip moves the index to skip the given byte.\nfunc (b *buffer) skip(bs byte) error {\n\tfor b.index \u003c b.length {\n\t\tif b.data[b.index] == bs \u0026\u0026 !b.backslash() {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn io.EOF\n}\n\n// skipAndReturnIndex moves the buffer index forward by one and returns the new index.\nfunc (b *buffer) skipAndReturnIndex() (int, error) {\n\terr := b.step()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn b.index, nil\n}\n\n// skipUntil moves the buffer index forward until it encounters a byte contained in the endTokens set.\nfunc (b *buffer) skipUntil(endTokens map[byte]bool) (int, error) {\n\tfor b.index \u003c b.length {\n\t\tcurrentByte, err := b.current()\n\t\tif err != nil {\n\t\t\treturn b.index, err\n\t\t}\n\n\t\t// Check if the current byte is in the set of end tokens.\n\t\tif _, exists := endTokens[currentByte]; exists {\n\t\t\treturn b.index, nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn b.index, io.EOF\n}\n\n// significantTokens is a map where the keys are the significant characters in a JSON path.\n// The values in the map are all true, which allows us to use the map as a set for quick lookups.\nvar significantTokens = [256]bool{\n\tdot:          true, // access properties of an object\n\tdollarSign:   true, // root object\n\tatSign:       true, // current object\n\tbracketOpen:  true, // start of an array index or filter expression\n\tbracketClose: true, // end of an array index or filter expression\n}\n\n// filterTokens stores the filter expression tokens.\nvar filterTokens = [256]bool{\n\taesterisk: true, // wildcard\n\tandSign:   true,\n\torSign:    true,\n}\n\n// skipToNextSignificantToken advances the buffer index to the next significant character.\n// Significant characters are defined based on the JSON path syntax.\nfunc (b *buffer) skipToNextSignificantToken() {\n\tfor b.index \u003c b.length {\n\t\tcurrent := b.data[b.index]\n\n\t\tif significantTokens[current] {\n\t\t\tbreak\n\t\t}\n\n\t\tb.index++\n\t}\n}\n\n// backslash checks to see if the number of backslashes before the current index is odd.\n//\n// This is used to check if the current character is escaped. However, unlike the \"unescape\" function,\n// \"backslash\" only serves to check the number of backslashes.\nfunc (b *buffer) backslash() bool {\n\tif b.index == 0 {\n\t\treturn false\n\t}\n\n\tcount := 0\n\tfor i := b.index - 1; ; i-- {\n\t\tif b.data[i] != backSlash {\n\t\t\tbreak\n\t\t}\n\n\t\tcount++\n\n\t\tif i == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn count%2 != 0\n}\n\n// numIndex holds a map of valid numeric characters\nvar numIndex = [256]bool{\n\t'0': true,\n\t'1': true,\n\t'2': true,\n\t'3': true,\n\t'4': true,\n\t'5': true,\n\t'6': true,\n\t'7': true,\n\t'8': true,\n\t'9': true,\n\t'.': true,\n\t'e': true,\n\t'E': true,\n}\n\n// pathToken checks if the current token is a valid JSON path token.\nfunc (b *buffer) pathToken() error {\n\tvar stack []byte\n\n\tinToken := false\n\tinNumber := false\n\tfirst := b.index\n\n\tfor b.index \u003c b.length {\n\t\tc := b.data[b.index]\n\n\t\tswitch {\n\t\tcase c == doubleQuote || c == singleQuote:\n\t\t\tinToken = true\n\t\t\tif err := b.step(); err != nil {\n\t\t\t\treturn errors.New(\"error stepping through buffer\")\n\t\t\t}\n\n\t\t\tif err := b.skip(c); err != nil {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\t\tif b.index \u003e= b.length {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\tcase c == bracketOpen || c == parenOpen:\n\t\t\tinToken = true\n\t\t\tstack = append(stack, c)\n\n\t\tcase c == bracketClose || c == parenClose:\n\t\t\tinToken = true\n\t\t\tif len(stack) == 0 || (c == bracketClose \u0026\u0026 stack[len(stack)-1] != bracketOpen) || (c == parenClose \u0026\u0026 stack[len(stack)-1] != parenOpen) {\n\t\t\t\treturn errUnmatchedParenthesis\n\t\t\t}\n\n\t\t\tstack = stack[:len(stack)-1]\n\n\t\tcase pathStateContainsValidPathToken(c):\n\t\t\tinToken = true\n\n\t\tcase c == plus || c == minus:\n\t\t\tif inNumber || (b.index \u003e 0 \u0026\u0026 numIndex[b.data[b.index-1]]) {\n\t\t\t\tinToken = true\n\t\t\t} else if !inToken \u0026\u0026 (b.index+1 \u003c b.length \u0026\u0026 numIndex[b.data[b.index+1]]) {\n\t\t\t\tinToken = true\n\t\t\t\tinNumber = true\n\t\t\t} else if !inToken {\n\t\t\t\treturn errInvalidToken\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif len(stack) != 0 || inToken {\n\t\t\t\tinToken = true\n\t\t\t} else {\n\t\t\t\tgoto end\n\t\t\t}\n\t\t}\n\n\t\tb.index++\n\t}\n\nend:\n\tif len(stack) != 0 {\n\t\treturn errUnmatchedParenthesis\n\t}\n\n\tif first == b.index {\n\t\treturn errors.New(\"no token found\")\n\t}\n\n\tif inNumber \u0026\u0026 !numIndex[b.data[b.index-1]] {\n\t\tinNumber = false\n\t}\n\n\treturn nil\n}\n\nfunc pathStateContainsValidPathToken(c byte) bool {\n\tif significantTokens[c] {\n\t\treturn true\n\t}\n\n\tif filterTokens[c] {\n\t\treturn true\n\t}\n\n\tif numIndex[c] {\n\t\treturn true\n\t}\n\n\tif 'A' \u003c= c \u0026\u0026 c \u003c= 'Z' || 'a' \u003c= c \u0026\u0026 c \u003c= 'z' {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (b *buffer) numeric(token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(doubleQuote)\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\tif token {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\treturn nil\n\t\t}\n\n\t\tif b.state \u003c MI || b.state \u003e E3 {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\tif b.last != ZE \u0026\u0026 b.last != IN \u0026\u0026 b.last != FR \u0026\u0026 b.last != E3 {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) getClasses(c byte) Classes {\n\tif b.data[b.index] \u003e= 128 {\n\t\treturn C_ETC\n\t}\n\n\tif c == singleQuote {\n\t\treturn QuoteAsciiClasses[b.data[b.index]]\n\t}\n\n\treturn AsciiClasses[b.data[b.index]]\n}\n\nfunc (b *buffer) getState() States {\n\tb.last = b.state\n\n\tb.class = b.getClasses(doubleQuote)\n\tif b.class == __ {\n\t\treturn __\n\t}\n\n\tb.state = StateTransitionTable[b.last][b.class]\n\n\treturn b.state\n}\n\n// string parses a string token from the buffer.\nfunc (b *buffer) string(search byte, token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(search)\n\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\tbreak\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) word(bs []byte) error {\n\tvar c byte\n\n\tmax := len(bs)\n\tindex := 0\n\n\tfor ; b.index \u003c b.length \u0026\u0026 index \u003c max; b.index++ {\n\t\tc = b.data[b.index]\n\n\t\tif c != bs[index] {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tindex++\n\t\tif index \u003e= max {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != max {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc numberKind2f64(value any) (result float64, err error) {\n\tswitch typed := value.(type) {\n\tcase float64:\n\t\tresult = typed\n\tcase float32:\n\t\tresult = float64(typed)\n\tcase int:\n\t\tresult = float64(typed)\n\tcase int8:\n\t\tresult = float64(typed)\n\tcase int16:\n\t\tresult = float64(typed)\n\tcase int32:\n\t\tresult = float64(typed)\n\tcase int64:\n\t\tresult = float64(typed)\n\tcase uint:\n\t\tresult = float64(typed)\n\tcase uint8:\n\t\tresult = float64(typed)\n\tcase uint16:\n\t\tresult = float64(typed)\n\tcase uint32:\n\t\tresult = float64(typed)\n\tcase uint64:\n\t\tresult = float64(typed)\n\tdefault:\n\t\terr = ufmt.Errorf(\"invalid number type: %T\", value)\n\t}\n\n\treturn\n}\n"},{"name":"builder.gno","body":"package json\n\ntype NodeBuilder struct {\n\tnode *Node\n}\n\nfunc Builder() *NodeBuilder {\n\treturn \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n}\n\nfunc (b *NodeBuilder) WriteString(key, value string) *NodeBuilder {\n\tb.node.AppendObject(key, StringNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNumber(key string, value float64) *NodeBuilder {\n\tb.node.AppendObject(key, NumberNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteBool(key string, value bool) *NodeBuilder {\n\tb.node.AppendObject(key, BoolNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNull(key string) *NodeBuilder {\n\tb.node.AppendObject(key, NullNode(\"\"))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteObject(key string, fn func(*NodeBuilder)) *NodeBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tb.node.AppendObject(key, nestedBuilder.node)\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteArray(key string, fn func(*ArrayBuilder)) *NodeBuilder {\n\tarrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(arrayBuilder)\n\tb.node.AppendObject(key, ArrayNode(\"\", arrayBuilder.nodes))\n\treturn b\n}\n\nfunc (b *NodeBuilder) Node() *Node {\n\treturn b.node\n}\n\ntype ArrayBuilder struct {\n\tnodes []*Node\n}\n\nfunc (ab *ArrayBuilder) WriteString(value string) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, StringNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNumber(value float64) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NumberNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteInt(value int) *ArrayBuilder {\n\treturn ab.WriteNumber(float64(value))\n}\n\nfunc (ab *ArrayBuilder) WriteBool(value bool) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, BoolNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNull() *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NullNode(\"\"))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteObject(fn func(*NodeBuilder)) *ArrayBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tab.nodes = append(ab.nodes, nestedBuilder.node)\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteArray(fn func(*ArrayBuilder)) *ArrayBuilder {\n\tnestedArrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(nestedArrayBuilder)\n\tab.nodes = append(ab.nodes, ArrayNode(\"\", nestedArrayBuilder.nodes))\n\treturn ab\n}\n"},{"name":"decode.gno","body":"// ref: https://github.com/spyzhov/ajson/blob/master/decode.go\n\npackage json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// This limits the max nesting depth to prevent stack overflow.\n// This is permitted by https://tools.ietf.org/html/rfc7159#section-9\nconst maxNestingDepth = 10000\n\n// Unmarshal parses the JSON-encoded data and returns a Node.\n// The data must be a valid JSON-encoded value.\n//\n// Usage:\n//\n//\tnode, err := json.Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err != nil {\n//\t\tufmt.Println(err)\n//\t}\n//\tprintln(node) // {\"key\": \"value\"}\nfunc Unmarshal(data []byte) (*Node, error) {\n\tbuf := newBuffer(data)\n\n\tvar (\n\t\tstate   States\n\t\tkey     *string\n\t\tcurrent *Node\n\t\tnesting int\n\t\tuseKey  = func() **string {\n\t\t\ttmp := cptrs(key)\n\t\t\tkey = nil\n\t\t\treturn \u0026tmp\n\t\t}\n\t\terr error\n\t)\n\n\tif _, err = buf.first(); err != nil {\n\t\treturn nil, io.EOF\n\t}\n\n\tfor {\n\t\tstate = buf.getState()\n\t\tif state == __ {\n\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t}\n\n\t\t// region state machine\n\t\tif state \u003e= GO {\n\t\t\tswitch buf.state {\n\t\t\tcase ST: // string\n\t\t\t\tif current != nil \u0026\u0026 current.IsObject() \u0026\u0026 key == nil {\n\t\t\t\t\t// key detected\n\t\t\t\t\tif key, err = getString(buf); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tbuf.state = CO\n\t\t\t\t} else {\n\t\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, String, nesting, useKey())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\terr = buf.string(doubleQuote, false)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\t\t\t\t\tbuf.state = OK\n\t\t\t\t}\n\n\t\t\tcase MI, ZE, IN: // number\n\t\t\t\tcurrent, err = processNumericNode(current, buf, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase T1, F1: // boolean\n\t\t\t\tliteral := falseLiteral\n\t\t\t\tif buf.state == T1 {\n\t\t\t\t\tliteral = trueLiteral\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Boolean, literal, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase N1: // null\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Null, nullLiteral, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// region action\n\t\t\tswitch state {\n\t\t\tcase ec, cc: // \u003cempty\u003e }\n\t\t\t\tif key != nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Object)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase bc: // ]\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Array)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase co, bo: // { [\n\t\t\t\tvalTyp, bState := Object, OB\n\t\t\t\tif state == bo {\n\t\t\t\t\tvalTyp, bState = Array, AR\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, valTyp, nesting, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.state = bState\n\n\t\t\tcase cm: // ,\n\t\t\t\tif current == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif !current.isContainer() {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif current.IsObject() {\n\t\t\t\t\tbuf.state = KE // key expected\n\t\t\t\t} else {\n\t\t\t\t\tbuf.state = VA // value expected\n\t\t\t\t}\n\n\t\t\tcase cl: // :\n\t\t\t\tif current == nil || !current.IsObject() || key == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tbuf.state = VA\n\n\t\t\tdefault:\n\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t}\n\t\t}\n\n\t\tif buf.step() != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif _, err = buf.first(); err != nil {\n\t\t\terr = nil\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif current == nil || buf.state != OK {\n\t\treturn nil, io.EOF\n\t}\n\n\troot := current.root()\n\tif !root.ready() {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn root, err\n}\n\n// UnmarshalSafe parses the JSON-encoded data and returns a Node.\nfunc UnmarshalSafe(data []byte) (*Node, error) {\n\tvar safe []byte\n\tsafe = append(safe, data...)\n\treturn Unmarshal(safe)\n}\n\n// processNumericNode creates a new node, processes a numeric value,\n// sets the node's borders, and moves to the previous node.\nfunc processNumericNode(current *Node, buf *buffer, key **string) (*Node, error) {\n\tvar err error\n\tcurrent, err = createNode(current, buf, Number, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = buf.numeric(false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcurrent.borders[1] = buf.index\n\tif current.prev != nil {\n\t\tcurrent = current.prev\n\t}\n\n\tbuf.index -= 1\n\tbuf.state = OK\n\n\treturn current, nil\n}\n\n// processLiteralNode creates a new node, processes a literal value,\n// sets the node's borders, and moves to the previous node.\nfunc processLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteralValue []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tcurrent, nesting, err = createLiteralNode(current, buf, literalType, literalValue, useKey, nesting)\n\tif err != nil {\n\t\treturn nil, nesting, err\n\t}\n\treturn current, nesting, nil\n}\n\n// isValidContainerType checks if the current node is a valid container (object or array).\n// The container must satisfy the following conditions:\n//  1. The current node must not be nil.\n//  2. The current node must be an object or array.\n//  3. The current node must not be ready.\nfunc isValidContainerType(current *Node, nodeType ValueType) bool {\n\tswitch nodeType {\n\tcase Object:\n\t\treturn current != nil \u0026\u0026 current.IsObject() \u0026\u0026 !current.ready()\n\tcase Array:\n\t\treturn current != nil \u0026\u0026 current.IsArray() \u0026\u0026 !current.ready()\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// getString extracts a string from the buffer and advances the buffer index past the string.\nfunc getString(b *buffer) (*string, error) {\n\tstart := b.index\n\tif err := b.string(doubleQuote, false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalue, ok := Unquote(b.data[start:b.index+1], doubleQuote)\n\tif !ok {\n\t\treturn nil, unexpectedTokenError(b.data, start)\n\t}\n\n\treturn \u0026value, nil\n}\n\n// createNode creates a new node and sets the key if it is not nil.\nfunc createNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tkey **string,\n) (*Node, error) {\n\tvar err error\n\tcurrent, err = NewNode(current, buf, nodeType, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn current, nil\n}\n\n// createNestedNode creates a new nested node (array or object) and sets the key if it is not nil.\nfunc createNestedNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tnesting int,\n\tkey **string,\n) (*Node, int, error) {\n\tvar err error\n\tif nesting, err = checkNestingDepth(nesting); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\tif current, err = createNode(current, buf, nodeType, key); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\treturn current, nesting, nil\n}\n\n// createLiteralNode creates a new literal node and sets the key if it is not nil.\n// The literal is a byte slice that represents a boolean or null value.\nfunc createLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteral []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tif current, err = createNode(current, buf, literalType, useKey); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tif err = buf.word(literal); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, false)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// updateNode updates the current node and returns the previous node.\nfunc updateNode(\n\tcurrent *Node, buf *buffer, nesting int, decreaseLevel bool,\n) (*Node, int) {\n\tcurrent.borders[1] = buf.index + 1\n\n\tprev := current.prev\n\tif prev == nil {\n\t\treturn current, nesting\n\t}\n\n\tcurrent = prev\n\tif decreaseLevel {\n\t\tnesting--\n\t}\n\n\treturn current, nesting\n}\n\n// updateNodeAndSetBufferState updates the current node and sets the buffer state to OK.\nfunc updateNodeAndSetBufferState(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnesting int,\n\ttyp ValueType,\n) (*Node, int, error) {\n\tif !isValidContainerType(current, typ) {\n\t\treturn nil, nesting, unexpectedTokenError(buf.data, buf.index)\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// checkNestingDepth checks if the nesting depth is within the maximum allowed depth.\nfunc checkNestingDepth(nesting int) (int, error) {\n\tif nesting \u003e= maxNestingDepth {\n\t\treturn nesting, errors.New(\"maximum nesting depth exceeded\")\n\t}\n\n\treturn nesting + 1, nil\n}\n\nfunc unexpectedTokenError(data []byte, index int) error {\n\treturn ufmt.Errorf(\"unexpected token at index %d. data %b\", index, data)\n}\n"},{"name":"encode.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Marshal returns the JSON encoding of a Node.\nfunc Marshal(node *Node) ([]byte, error) {\n\tvar (\n\t\tbuf  bytes.Buffer\n\t\tsVal string\n\t\tbVal bool\n\t\tnVal float64\n\t\toVal []byte\n\t\terr  error\n\t)\n\n\tif node == nil {\n\t\treturn nil, errors.New(\"node is nil\")\n\t}\n\n\tif !node.modified \u0026\u0026 !node.ready() {\n\t\treturn nil, errors.New(\"node is not ready\")\n\t}\n\n\tif !node.modified \u0026\u0026 node.ready() {\n\t\tbuf.Write(node.source())\n\t}\n\n\tif node.modified {\n\t\tswitch node.nodeType {\n\t\tcase Null:\n\t\t\tbuf.Write(nullLiteral)\n\n\t\tcase Number:\n\t\t\tnVal, err = node.GetNumeric()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnum := strconv.FormatFloat(nVal, 'f', -1, 64)\n\t\t\tbuf.WriteString(num)\n\n\t\tcase String:\n\t\t\tsVal, err = node.GetString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tquoted := ufmt.Sprintf(\"%s\", strconv.Quote(sVal))\n\t\t\tbuf.WriteString(quoted)\n\n\t\tcase Boolean:\n\t\t\tbVal, err = node.GetBool()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbStr := ufmt.Sprintf(\"%t\", bVal)\n\t\t\tbuf.WriteString(bStr)\n\n\t\tcase Array:\n\t\t\tbuf.WriteByte(bracketOpen)\n\n\t\t\tfor i := 0; i \u003c len(node.next); i++ {\n\t\t\t\tif i != 0 {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t}\n\n\t\t\t\telem, ok := node.next[strconv.Itoa(i)]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, ufmt.Errorf(\"array element %d is not found\", i)\n\t\t\t\t}\n\n\t\t\t\toVal, err = Marshal(elem)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(bracketClose)\n\n\t\tcase Object:\n\t\t\tbuf.WriteByte(curlyOpen)\n\n\t\t\tbVal = false\n\t\t\tfor k, v := range node.next {\n\t\t\t\tif bVal {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t} else {\n\t\t\t\t\tbVal = true\n\t\t\t\t}\n\n\t\t\t\tkey := ufmt.Sprintf(\"%s\", strconv.Quote(k))\n\t\t\t\tbuf.WriteString(key)\n\t\t\t\tbuf.WriteByte(colon)\n\n\t\t\t\toVal, err = Marshal(v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(curlyClose)\n\t\t}\n\t}\n\n\treturn buf.Bytes(), nil\n}\n"},{"name":"errors.gno","body":"package json\n\nimport \"errors\"\n\nvar (\n\terrNilNode               = errors.New(\"node is nil\")\n\terrNotArrayNode          = errors.New(\"node is not array\")\n\terrNotBoolNode           = errors.New(\"node is not boolean\")\n\terrNotNullNode           = errors.New(\"node is not null\")\n\terrNotNumberNode         = errors.New(\"node is not number\")\n\terrNotObjectNode         = errors.New(\"node is not object\")\n\terrNotStringNode         = errors.New(\"node is not string\")\n\terrInvalidToken          = errors.New(\"invalid token\")\n\terrIndexNotFound         = errors.New(\"index not found\")\n\terrInvalidAppend         = errors.New(\"can't append value to non-appendable node\")\n\terrInvalidAppendCycle    = errors.New(\"appending value to itself or its children or parents will cause a cycle\")\n\terrInvalidEscapeSequence = errors.New(\"invalid escape sequence\")\n\terrInvalidStringValue    = errors.New(\"invalid string value\")\n\terrEmptyBooleanNode      = errors.New(\"boolean node is empty\")\n\terrEmptyStringNode       = errors.New(\"string node is empty\")\n\terrKeyRequired           = errors.New(\"key is required for object\")\n\terrUnmatchedParenthesis  = errors.New(\"mismatched bracket or parenthesis\")\n\terrUnmatchedQuotePath    = errors.New(\"unmatched quote in path\")\n)\n\nvar (\n\terrInvalidStringInput    = errors.New(\"invalid string input\")\n\terrMalformedBooleanValue = errors.New(\"malformed boolean value\")\n\terrEmptyByteSlice        = errors.New(\"empty byte slice\")\n\terrInvalidExponentValue  = errors.New(\"invalid exponent value\")\n\terrNonDigitCharacters    = errors.New(\"non-digit characters found\")\n\terrNumericRangeExceeded  = errors.New(\"numeric value exceeds the range limit\")\n\terrMultipleDecimalPoints = errors.New(\"multiple decimal points found\")\n)\n"},{"name":"escape.gno","body":"package json\n\nimport (\n\t\"unicode/utf8\"\n)\n\nconst (\n\tsupplementalPlanesOffset     = 0x10000\n\thighSurrogateOffset          = 0xD800\n\tlowSurrogateOffset           = 0xDC00\n\tsurrogateEnd                 = 0xDFFF\n\tbasicMultilingualPlaneOffset = 0xFFFF\n\tbadHex                       = -1\n\n\tsingleUnicodeEscapeLen = 6\n\tsurrogatePairLen       = 12\n)\n\nvar hexLookupTable = [256]int{\n\t'0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, '4': 0x4,\n\t'5': 0x5, '6': 0x6, '7': 0x7, '8': 0x8, '9': 0x9,\n\t'A': 0xA, 'B': 0xB, 'C': 0xC, 'D': 0xD, 'E': 0xE, 'F': 0xF,\n\t'a': 0xA, 'b': 0xB, 'c': 0xC, 'd': 0xD, 'e': 0xE, 'f': 0xF,\n\t// Fill unspecified index-value pairs with key and value of -1\n\t'G': -1, 'H': -1, 'I': -1, 'J': -1,\n\t'K': -1, 'L': -1, 'M': -1, 'N': -1,\n\t'O': -1, 'P': -1, 'Q': -1, 'R': -1,\n\t'S': -1, 'T': -1, 'U': -1, 'V': -1,\n\t'W': -1, 'X': -1, 'Y': -1, 'Z': -1,\n\t'g': -1, 'h': -1, 'i': -1, 'j': -1,\n\t'k': -1, 'l': -1, 'm': -1, 'n': -1,\n\t'o': -1, 'p': -1, 'q': -1, 'r': -1,\n\t's': -1, 't': -1, 'u': -1, 'v': -1,\n\t'w': -1, 'x': -1, 'y': -1, 'z': -1,\n}\n\nfunc h2i(c byte) int {\n\treturn hexLookupTable[c]\n}\n\n// Unescape takes an input byte slice, processes it to Unescape certain characters,\n// and writes the result into an output byte slice.\n//\n// it returns the processed slice and any error encountered during the Unescape operation.\nfunc Unescape(input, output []byte) ([]byte, error) {\n\t// ensure the output slice has enough capacity to hold the input slice.\n\tinputLen := len(input)\n\tif cap(output) \u003c inputLen {\n\t\toutput = make([]byte, inputLen)\n\t}\n\n\tinPos, outPos := 0, 0\n\n\tfor inPos \u003c len(input) {\n\t\tc := input[inPos]\n\t\tif c != backSlash {\n\t\t\toutput[outPos] = c\n\t\t\tinPos++\n\t\t\toutPos++\n\t\t} else {\n\t\t\t// process escape sequence\n\t\t\tinLen, outLen, err := processEscapedUTF8(input[inPos:], output[outPos:])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tinPos += inLen\n\t\t\toutPos += outLen\n\t\t}\n\t}\n\n\treturn output[:outPos], nil\n}\n\n// isSurrogatePair returns true if the rune is a surrogate pair.\n//\n// A surrogate pairs are used in UTF-16 encoding to encode characters\n// outside the Basic Multilingual Plane (BMP).\nfunc isSurrogatePair(r rune) bool {\n\treturn highSurrogateOffset \u003c= r \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// isHighSurrogate checks if the rune is a high surrogate (U+D800 to U+DBFF).\nfunc isHighSurrogate(r rune) bool {\n\treturn r \u003e= highSurrogateOffset \u0026\u0026 r \u003c= 0xDBFF\n}\n\n// isLowSurrogate checks if the rune is a low surrogate (U+DC00 to U+DFFF).\nfunc isLowSurrogate(r rune) bool {\n\treturn r \u003e= lowSurrogateOffset \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// combineSurrogates reconstruct the original unicode code points in the\n// supplemental plane by combinin the high and low surrogate.\n//\n// The hight surrogate in the range from U+D800 to U+DBFF,\n// and the low surrogate in the range from U+DC00 to U+DFFF.\n//\n// The formula to combine the surrogates is:\n// (high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000\nfunc combineSurrogates(high, low rune) rune {\n\treturn ((high - highSurrogateOffset) \u003c\u003c 10) + (low - lowSurrogateOffset) + supplementalPlanesOffset\n}\n\n// deocdeSingleUnicodeEscape decodes a unicode escape sequence (e.g., \\uXXXX) into a rune.\nfunc decodeSingleUnicodeEscape(b []byte) (rune, bool) {\n\tif len(b) \u003c 6 {\n\t\treturn utf8.RuneError, false\n\t}\n\n\t// convert hex to decimal\n\th1, h2, h3, h4 := h2i(b[2]), h2i(b[3]), h2i(b[4]), h2i(b[5])\n\tif h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex {\n\t\treturn utf8.RuneError, false\n\t}\n\n\treturn rune(h1\u003c\u003c12 + h2\u003c\u003c8 + h3\u003c\u003c4 + h4), true\n}\n\n// decodeUnicodeEscape decodes a Unicode escape sequence from a byte slice.\n// It handles both single Unicode escape sequences and surrogate pairs.\nfunc decodeUnicodeEscape(b []byte) (rune, int) {\n\t// decode the first Unicode escape sequence.\n\tr, ok := decodeSingleUnicodeEscape(b)\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is within the BMP and not a surrogate, return it\n\tif r \u003c= basicMultilingualPlaneOffset \u0026\u0026 !isSurrogatePair(r) {\n\t\treturn r, 6\n\t}\n\n\tif !isHighSurrogate(r) {\n\t\t// invalid surrogate pair.\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is a high surrogate, need to decode the next escape sequence.\n\n\t// ensure there are enough bytes for the next escape sequence.\n\tif len(b) \u003c surrogatePairLen {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// decode the second Unicode escape sequence.\n\tr2, ok := decodeSingleUnicodeEscape(b[singleUnicodeEscapeLen:])\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// check if the second rune is a low surrogate.\n\tif isLowSurrogate(r2) {\n\t\tcombined := combineSurrogates(r, r2)\n\t\treturn combined, surrogatePairLen\n\t}\n\treturn utf8.RuneError, -1\n}\n\nvar escapeByteSet = [256]byte{\n\t'\"':  doubleQuote,\n\t'\\\\': backSlash,\n\t'/':  slash,\n\t'b':  backSpace,\n\t'f':  formFeed,\n\t'n':  newLine,\n\t'r':  carriageReturn,\n\t't':  tab,\n}\n\n// Unquote takes a byte slice and unquotes it by removing\n// the surrounding quotes and unescaping the contents.\nfunc Unquote(s []byte, border byte) (string, bool) {\n\ts, ok := unquoteBytes(s, border)\n\treturn string(s), ok\n}\n\n// unquoteBytes takes a byte slice and unquotes it by removing\nfunc unquoteBytes(s []byte, border byte) ([]byte, bool) {\n\tif len(s) \u003c 2 || s[0] != border || s[len(s)-1] != border {\n\t\treturn nil, false\n\t}\n\n\ts = s[1 : len(s)-1]\n\n\tr := 0\n\tfor r \u003c len(s) {\n\t\tc := s[r]\n\n\t\tif c == backSlash || c == border || c \u003c 0x20 {\n\t\t\tbreak\n\t\t}\n\n\t\tif c \u003c utf8.RuneSelf {\n\t\t\tr++\n\t\t\tcontinue\n\t\t}\n\n\t\trr, size := utf8.DecodeRune(s[r:])\n\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\tbreak\n\t\t}\n\n\t\tr += size\n\t}\n\n\tif r == len(s) {\n\t\treturn s, true\n\t}\n\n\tutfDoubleMax := utf8.UTFMax * 2\n\tb := make([]byte, len(s)+utfDoubleMax)\n\tw := copy(b, s[0:r])\n\n\tfor r \u003c len(s) {\n\t\tif w \u003e= len(b)-utf8.UTFMax {\n\t\t\tnb := make([]byte, utfDoubleMax+(2*len(b)))\n\t\t\tcopy(nb, b)\n\t\t\tb = nb\n\t\t}\n\n\t\tc := s[r]\n\t\tif c == backSlash {\n\t\t\tr++\n\t\t\tif r \u003e= len(s) {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tif s[r] == 'u' {\n\t\t\t\trr, res := decodeUnicodeEscape(s[r-1:])\n\t\t\t\tif res \u003c 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t\t\tr += 5\n\t\t\t} else {\n\t\t\t\tdecode := escapeByteSet[s[r]]\n\t\t\t\tif decode == 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tif decode == doubleQuote || decode == backSlash || decode == slash {\n\t\t\t\t\tdecode = s[r]\n\t\t\t\t}\n\n\t\t\t\tb[w] = decode\n\t\t\t\tr++\n\t\t\t\tw++\n\t\t\t}\n\t\t} else if c == border || c \u003c 0x20 {\n\t\t\treturn nil, false\n\t\t} else if c \u003c utf8.RuneSelf {\n\t\t\tb[w] = c\n\t\t\tr++\n\t\t\tw++\n\t\t} else {\n\t\t\trr, size := utf8.DecodeRune(s[r:])\n\n\t\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tr += size\n\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t}\n\t}\n\n\treturn b[:w], true\n}\n\n// processEscapedUTF8 converts escape sequences to UTF-8 characters.\n// It decodes Unicode escape sequences (\\uXXXX) to UTF-8 and\n// converts standard escape sequences (e.g., \\n) to their corresponding special characters.\nfunc processEscapedUTF8(in, out []byte) (int, int, error) {\n\tif len(in) \u003c 2 || in[0] != backSlash {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\tescapeSeqLen := 2\n\tescapeChar := in[1]\n\n\tif escapeChar != 'u' {\n\t\tval := escapeByteSet[escapeChar]\n\t\tif val == 0 {\n\t\t\treturn -1, -1, errInvalidEscapeSequence\n\t\t}\n\n\t\tout[0] = val\n\t\treturn escapeSeqLen, 1, nil\n\t}\n\n\tr, size := decodeUnicodeEscape(in)\n\tif size == -1 {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\toutLen := utf8.EncodeRune(out, r)\n\n\treturn size, outLen, nil\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/onbloc/json\"\ngno = \"0.9\"\n"},{"name":"indent.gno","body":"package json\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n)\n\n// indentGrowthFactor specifies the growth factor of indenting JSON input.\n// A factor no higher than 2 ensures that wasted space never exceeds 50%.\nconst indentGrowthFactor = 2\n\n// IndentJSON formats the JSON data with the specified indentation.\nfunc Indent(data []byte, indent string) ([]byte, error) {\n\tvar (\n\t\tout        bytes.Buffer\n\t\tlevel      int\n\t\tinArray    bool\n\t\tarrayDepth int\n\t)\n\n\tfor i := 0; i \u003c len(data); i++ {\n\t\tc := data[i] // current character\n\n\t\tswitch c {\n\t\tcase bracketOpen:\n\t\t\tarrayDepth++\n\t\t\tif arrayDepth \u003e 1 {\n\t\t\t\tlevel++ // increase the level if it's nested array\n\t\t\t\tinArray = true\n\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// case of the top-level array\n\t\t\t\tinArray = true\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase bracketClose:\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tlevel--\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tarrayDepth--\n\t\t\tif arrayDepth == 0 {\n\t\t\t\tinArray = false\n\t\t\t}\n\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase curlyOpen:\n\t\t\t// check if the empty object or array\n\t\t\t// we don't need to apply the indent when it's empty containers.\n\t\t\tif i+1 \u003c len(data) \u0026\u0026 data[i+1] == curlyClose {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\ti++ // skip next character\n\t\t\t\tif err := out.WriteByte(data[i]); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tlevel++\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase curlyClose:\n\t\t\tlevel--\n\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase comma, colon:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else if c == colon {\n\t\t\t\tif err := out.WriteByte(' '); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out.Bytes(), nil\n}\n\nfunc writeNewlineAndIndent(out *bytes.Buffer, level int, indent string) error {\n\tif err := out.WriteByte('\\n'); err != nil {\n\t\treturn err\n\t}\n\n\tidt := strings.Repeat(indent, level*indentGrowthFactor)\n\tif _, err := out.WriteString(idt); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"},{"name":"internal.gno","body":"package json\n\n// Reference: https://github.com/freddierice/php_source/blob/467ed5d6edff72219afd3e644516f131118ef48e/ext/json/JSON_parser.c\n// Copyright (c) 2005 JSON.org\n\n// Go implementation is taken from: https://github.com/spyzhov/ajson/blob/master/internal/state.go\n\ntype (\n\tStates  int8 // possible states of the parser\n\tClasses int8 // JSON string character types\n)\n\nconst __ = -1\n\n// enum classes\nconst (\n\tC_SPACE Classes = iota /* space */\n\tC_WHITE                /* other whitespace */\n\tC_LCURB                /* {  */\n\tC_RCURB                /* } */\n\tC_LSQRB                /* [ */\n\tC_RSQRB                /* ] */\n\tC_COLON                /* : */\n\tC_COMMA                /* , */\n\tC_QUOTE                /* \" */\n\tC_BACKS                /* \\ */\n\tC_SLASH                /* / */\n\tC_PLUS                 /* + */\n\tC_MINUS                /* - */\n\tC_POINT                /* . */\n\tC_ZERO                 /* 0 */\n\tC_DIGIT                /* 123456789 */\n\tC_LOW_A                /* a */\n\tC_LOW_B                /* b */\n\tC_LOW_C                /* c */\n\tC_LOW_D                /* d */\n\tC_LOW_E                /* e */\n\tC_LOW_F                /* f */\n\tC_LOW_L                /* l */\n\tC_LOW_N                /* n */\n\tC_LOW_R                /* r */\n\tC_LOW_S                /* s */\n\tC_LOW_T                /* t */\n\tC_LOW_U                /* u */\n\tC_ABCDF                /* ABCDF */\n\tC_E                    /* E */\n\tC_ETC                  /* everything else */\n)\n\n// AsciiClasses array maps the 128 ASCII characters into character classes.\nvar AsciiClasses = [128]Classes{\n\t/*\n\t   This array maps the 128 ASCII characters into character classes.\n\t   The remaining Unicode characters should be mapped to C_ETC.\n\t   Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_QUOTE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n// QuoteAsciiClasses is a HACK for single quote from AsciiClasses\nvar QuoteAsciiClasses = [128]Classes{\n\t/*\n\t   This array maps the 128 ASCII characters into character classes.\n\t   The remaining Unicode characters should be mapped to C_ETC.\n\t   Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_QUOTE,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n/*\nThe state codes.\n*/\nconst (\n\tGO States = iota /* start    */\n\tOK               /* ok       */\n\tOB               /* object   */\n\tKE               /* key      */\n\tCO               /* colon    */\n\tVA               /* value    */\n\tAR               /* array    */\n\tST               /* string   */\n\tES               /* escape   */\n\tU1               /* u1       */\n\tU2               /* u2       */\n\tU3               /* u3       */\n\tU4               /* u4       */\n\tMI               /* minus    */\n\tZE               /* zero     */\n\tIN               /* integer  */\n\tDT               /* dot      */\n\tFR               /* fraction */\n\tE1               /* e        */\n\tE2               /* ex       */\n\tE3               /* exp      */\n\tT1               /* tr       */\n\tT2               /* tru      */\n\tT3               /* true     */\n\tF1               /* fa       */\n\tF2               /* fal      */\n\tF3               /* fals     */\n\tF4               /* false    */\n\tN1               /* nu       */\n\tN2               /* nul      */\n\tN3               /* null     */\n)\n\n// List of action codes.\n// these constants are defining an action that should be performed under certain conditions.\nconst (\n\tcl States = -2 /* colon           */\n\tcm States = -3 /* comma           */\n\tqt States = -4 /* quote           */\n\tbo States = -5 /* bracket open    */\n\tco States = -6 /* curly bracket open  */\n\tbc States = -7 /* bracket close   */\n\tcc States = -8 /* curly bracket close */\n\tec States = -9 /* curly bracket empty */\n)\n\n// StateTransitionTable is the state transition table takes the current state and the current symbol, and returns either\n// a new state or an action. An action is represented as a negative number. A JSON text is accepted if at the end of the\n// text the state is OK and if the mode is DONE.\nvar StateTransitionTable = [31][31]States{\n\t/*\n\t   The state transition table takes the current state and the current symbol,\n\t   and returns either a new state or an action. An action is represented as a\n\t   negative number. A JSON text is accepted if at the end of the text the\n\t   state is OK and if the mode is DONE.\n\t                  white                                                    1-9                                                ABCDF   etc\n\t            space   |   {   }   [   ]   :   ,   \"   \\   /   +   -   .   0   |   a   b   c   d   e   f   l   n   r   s   t   u   |   E   |*/\n\t/*start  GO*/ {GO, GO, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*ok     OK*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*object OB*/ {OB, OB, __, ec, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*key    KE*/ {KE, KE, __, __, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*colon  CO*/ {CO, CO, __, __, __, __, cl, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*value  VA*/ {VA, VA, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*array  AR*/ {AR, AR, co, __, bo, bc, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*string ST*/ {ST, __, ST, ST, ST, ST, ST, ST, qt, ES, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST},\n\t/*escape ES*/ {__, __, __, __, __, __, __, __, ST, ST, ST, __, __, __, __, __, __, ST, __, __, __, ST, __, ST, ST, __, ST, U1, __, __, __},\n\t/*u1     U1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U2, U2, U2, U2, U2, U2, U2, U2, __, __, __, __, __, __, U2, U2, __},\n\t/*u2     U2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U3, U3, U3, U3, U3, U3, U3, U3, __, __, __, __, __, __, U3, U3, __},\n\t/*u3     U3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U4, U4, U4, U4, U4, U4, U4, U4, __, __, __, __, __, __, U4, U4, __},\n\t/*u4     U4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ST, ST, ST, ST, ST, ST, ST, ST, __, __, __, __, __, __, ST, ST, __},\n\t/*minus  MI*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ZE, IN, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*zero   ZE*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, __, __, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*int    IN*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, IN, IN, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*dot    DT*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, FR, FR, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*frac   FR*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, FR, FR, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*e      E1*/ {__, __, __, __, __, __, __, __, __, __, __, E2, E2, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*ex     E2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*exp    E3*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*tr     T1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T2, __, __, __, __, __, __},\n\t/*tru    T2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T3, __, __, __},\n\t/*true   T3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*fa     F1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F2, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*fal    F2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F3, __, __, __, __, __, __, __, __},\n\t/*fals   F3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F4, __, __, __, __, __},\n\t/*false  F4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*nu     N1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N2, __, __, __},\n\t/*nul    N2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N3, __, __, __, __, __, __, __, __},\n\t/*null   N3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __},\n}\n"},{"name":"node.gno","body":"package json\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Node represents a JSON node.\ntype Node struct {\n\tprev     *Node            // prev is the parent node of the current node.\n\tnext     map[string]*Node // next is the child nodes of the current node.\n\tkey      *string          // key holds the key of the current node in the parent node.\n\tdata     []byte           // byte slice of JSON data\n\tvalue    any              // value holds the value of the current node.\n\tnodeType ValueType        // NodeType holds the type of the current node. (Object, Array, String, Number, Boolean, Null)\n\tindex    *int             // index holds the index of the current node in the parent array node.\n\tborders  [2]int           // borders stores the start and end index of the current node in the data.\n\tmodified bool             // modified indicates the current node is changed or not.\n}\n\n// NewNode creates a new node instance with the given parent node, buffer, type, and key.\nfunc NewNode(prev *Node, b *buffer, typ ValueType, key **string) (*Node, error) {\n\tcurr := \u0026Node{\n\t\tprev:     prev,\n\t\tdata:     b.data,\n\t\tborders:  [2]int{b.index, 0},\n\t\tkey:      *key,\n\t\tnodeType: typ,\n\t\tmodified: false,\n\t}\n\n\tif typ == Object || typ == Array {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\tif prev != nil {\n\t\tif prev.IsArray() {\n\t\t\tsize := len(prev.next)\n\t\t\tcurr.index = \u0026size\n\n\t\t\tprev.next[strconv.Itoa(size)] = curr\n\t\t} else if prev.IsObject() {\n\t\t\tif key == nil {\n\t\t\t\treturn nil, errKeyRequired\n\t\t\t}\n\n\t\t\tprev.next[**key] = curr\n\t\t} else {\n\t\t\treturn nil, errors.New(\"invalid parent type\")\n\t\t}\n\t}\n\n\treturn curr, nil\n}\n\n// load retrieves the value of the current node.\nfunc (n *Node) load() any {\n\treturn n.value\n}\n\n// Changed checks the current node is changed or not.\nfunc (n *Node) Changed() bool {\n\treturn n.modified\n}\n\n// Key returns the key of the current node.\nfunc (n *Node) Key() string {\n\tif n == nil || n.key == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *n.key\n}\n\n// HasKey checks the current node has the given key or not.\nfunc (n *Node) HasKey(key string) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\t_, ok := n.next[key]\n\treturn ok\n}\n\n// GetKey returns the value of the given key from the current object node.\nfunc (n *Node) GetKey(key string) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.Type() != Object {\n\t\treturn nil, ufmt.Errorf(\"target node is not object type. got: %s\", n.Type().String())\n\t}\n\n\tvalue, ok := n.next[key]\n\tif !ok {\n\t\treturn nil, ufmt.Errorf(\"key not found: %s\", key)\n\t}\n\n\treturn value, nil\n}\n\n// MustKey returns the value of the given key from the current object node.\nfunc (n *Node) MustKey(key string) *Node {\n\tval, err := n.GetKey(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// UniqueKeyLists traverses the current JSON nodes and collects all the unique keys.\nfunc (n *Node) UniqueKeyLists() []string {\n\tvar collectKeys func(*Node) []string\n\tcollectKeys = func(node *Node) []string {\n\t\tif node == nil || !node.IsObject() {\n\t\t\treturn nil\n\t\t}\n\n\t\tresult := make(map[string]bool)\n\t\tfor key, childNode := range node.next {\n\t\t\tresult[key] = true\n\t\t\tchildKeys := collectKeys(childNode)\n\t\t\tfor _, childKey := range childKeys {\n\t\t\t\tresult[childKey] = true\n\t\t\t}\n\t\t}\n\n\t\tkeys := make([]string, 0, len(result))\n\t\tfor key := range result {\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\treturn keys\n\t}\n\n\treturn collectKeys(n)\n}\n\n// Empty returns true if the current node is empty.\nfunc (n *Node) Empty() bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\treturn len(n.next) == 0\n}\n\n// Type returns the type (ValueType) of the current node.\nfunc (n *Node) Type() ValueType {\n\treturn n.nodeType\n}\n\n// Value returns the value of the current node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tval, err := root.MustKey(\"key\").Value()\n//\tif err != nil {\n//\t\tt.Errorf(\"Value returns error: %v\", err)\n//\t}\n//\n//\tresult: \"value\"\nfunc (n *Node) Value() (value any, err error) {\n\tvalue = n.load()\n\n\tif value == nil {\n\t\tswitch n.nodeType {\n\t\tcase Null:\n\t\t\treturn nil, nil\n\n\t\tcase Number:\n\t\t\tvalue, err = strconv.ParseFloat(string(n.source()), 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase String:\n\t\t\tvar ok bool\n\t\t\tvalue, ok = Unquote(n.source(), doubleQuote)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", errInvalidStringValue\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase Boolean:\n\t\t\tif len(n.source()) == 0 {\n\t\t\t\treturn nil, errEmptyBooleanNode\n\t\t\t}\n\n\t\t\tb := n.source()[0]\n\t\t\tvalue = b == 't' || b == 'T'\n\t\t\tn.value = value\n\n\t\tcase Array:\n\t\t\telems := make([]*Node, len(n.next))\n\n\t\t\tfor _, e := range n.next {\n\t\t\t\telems[*e.index] = e\n\t\t\t}\n\n\t\t\tvalue = elems\n\t\t\tn.value = value\n\n\t\tcase Object:\n\t\t\tobj := make(map[string]*Node, len(n.next))\n\n\t\t\tfor k, v := range n.next {\n\t\t\t\tobj[k] = v\n\t\t\t}\n\n\t\t\tvalue = obj\n\t\t\tn.value = value\n\t\t}\n\t}\n\n\treturn value, nil\n}\n\n// Delete removes the current node from the parent node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err := root.MustKey(\"key\").Delete(); err != nil {\n//\t\tt.Errorf(\"Delete returns error: %v\", err)\n//\t}\n//\n//\tresult: {} (empty object)\nfunc (n *Node) Delete() error {\n\tif n == nil {\n\t\treturn errors.New(\"can't delete nil node\")\n\t}\n\n\tif n.prev == nil {\n\t\treturn nil\n\t}\n\n\treturn n.prev.remove(n)\n}\n\n// Size returns the size (length) of the current array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.Size() != 2 {\n//\t\tt.Errorf(\"ArrayNode returns wrong size: %d\", root.Size())\n//\t}\nfunc (n *Node) Size() int {\n\tif n == nil {\n\t\treturn 0\n\t}\n\n\treturn len(n.next)\n}\n\n// Index returns the index of the current node in the parent array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.MustIndex(1).Index() != 1 {\n//\t\tt.Errorf(\"Index returns wrong index: %d\", root.MustIndex(1).Index())\n//\t}\n//\n// We can also use the index to the byte slice of the JSON data directly.\n//\n// Example:\n//\n//\troot := Unmarshal([]byte(`[\"foo\", 1]`))\n//\tif root == nil {\n//\t\tt.Errorf(\"Unmarshal returns nil\")\n//\t}\n//\n//\tif string(root.MustIndex(1).source()) != \"1\" {\n//\t\tt.Errorf(\"source returns wrong result: %s\", root.MustIndex(1).source())\n//\t}\nfunc (n *Node) Index() int {\n\tif n == nil || n.index == nil {\n\t\treturn -1\n\t}\n\n\treturn *n.index\n}\n\n// MustIndex returns the array element at the given index.\n//\n// If the index is negative, it returns the index is from the end of the array.\n// Also, it panics if the index is not found.\n//\n// check the Index method for detailed usage.\nfunc (n *Node) MustIndex(expectIdx int) *Node {\n\tval, err := n.GetIndex(expectIdx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// GetIndex returns the array element at the given index.\n//\n// if the index is negative, it returns the index is from the end of the array.\nfunc (n *Node) GetIndex(idx int) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsArray() {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tif idx \u003e n.Size() {\n\t\treturn nil, errors.New(\"input index exceeds the array size\")\n\t}\n\n\tif idx \u003c 0 {\n\t\tidx += len(n.next)\n\t}\n\n\tchild, ok := n.next[strconv.Itoa(idx)]\n\tif !ok {\n\t\treturn nil, errIndexNotFound\n\t}\n\n\treturn child, nil\n}\n\n// DeleteIndex removes the array element at the given index.\nfunc (n *Node) DeleteIndex(idx int) error {\n\tnode, err := n.GetIndex(idx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn n.remove(node)\n}\n\n// NullNode creates a new null type node.\n//\n// Usage:\n//\n//\t_ := NullNode(\"\")\nfunc NullNode(key string) *Node {\n\treturn \u0026Node{\n\t\tkey:      \u0026key,\n\t\tvalue:    nil,\n\t\tnodeType: Null,\n\t\tmodified: true,\n\t}\n}\n\n// NumberNode creates a new number type node.\n//\n// Usage:\n//\n//\troot := NumberNode(\"\", 1)\n//\tif root == nil {\n//\t\tt.Errorf(\"NumberNode returns nil\")\n//\t}\nfunc NumberNode(key string, value float64) *Node {\n\treturn \u0026Node{\n\t\tkey:      \u0026key,\n\t\tvalue:    value,\n\t\tnodeType: Number,\n\t\tmodified: true,\n\t}\n}\n\n// StringNode creates a new string type node.\n//\n// Usage:\n//\n//\troot := StringNode(\"\", \"foo\")\n//\tif root == nil {\n//\t\tt.Errorf(\"StringNode returns nil\")\n//\t}\nfunc StringNode(key string, value string) *Node {\n\treturn \u0026Node{\n\t\tkey:      \u0026key,\n\t\tvalue:    value,\n\t\tnodeType: String,\n\t\tmodified: true,\n\t}\n}\n\n// BoolNode creates a new given boolean value node.\n//\n// Usage:\n//\n//\troot := BoolNode(\"\", true)\n//\tif root == nil {\n//\t\tt.Errorf(\"BoolNode returns nil\")\n//\t}\nfunc BoolNode(key string, value bool) *Node {\n\treturn \u0026Node{\n\t\tkey:      \u0026key,\n\t\tvalue:    value,\n\t\tnodeType: Boolean,\n\t\tmodified: true,\n\t}\n}\n\n// ArrayNode creates a new array type node.\n//\n// If the given value is nil, it creates an empty array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\nfunc ArrayNode(key string, value []*Node) *Node {\n\tcurr := \u0026Node{\n\t\tkey:      \u0026key,\n\t\tnodeType: Array,\n\t\tmodified: true,\n\t}\n\n\tcurr.next = make(map[string]*Node, len(value))\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor i, v := range value {\n\t\t\tidx := i\n\t\t\tcurr.next[strconv.Itoa(i)] = v\n\n\t\t\tv.prev = curr\n\t\t\tv.index = \u0026idx\n\t\t}\n\t}\n\n\treturn curr\n}\n\n// ObjectNode creates a new object type node.\n//\n// If the given value is nil, it creates an empty object node.\n//\n// next is a map of key and value pairs of the object.\nfunc ObjectNode(key string, value map[string]*Node) *Node {\n\tcurr := \u0026Node{\n\t\tnodeType: Object,\n\t\tkey:      \u0026key,\n\t\tnext:     value,\n\t\tmodified: true,\n\t}\n\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor key, val := range value {\n\t\t\tvkey := key\n\t\t\tval.prev = curr\n\t\t\tval.key = \u0026vkey\n\t\t}\n\t} else {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\treturn curr\n}\n\n// IsArray returns true if the current node is array type.\nfunc (n *Node) IsArray() bool {\n\treturn n.nodeType == Array\n}\n\n// IsObject returns true if the current node is object type.\nfunc (n *Node) IsObject() bool {\n\treturn n.nodeType == Object\n}\n\n// IsNull returns true if the current node is null type.\nfunc (n *Node) IsNull() bool {\n\treturn n.nodeType == Null\n}\n\n// IsBool returns true if the current node is boolean type.\nfunc (n *Node) IsBool() bool {\n\treturn n.nodeType == Boolean\n}\n\n// IsString returns true if the current node is string type.\nfunc (n *Node) IsString() bool {\n\treturn n.nodeType == String\n}\n\n// IsNumber returns true if the current node is number type.\nfunc (n *Node) IsNumber() bool {\n\treturn n.nodeType == Number\n}\n\n// ready checks the current node is ready or not.\n//\n// the meaning of ready is the current node is parsed and has a valid value.\nfunc (n *Node) ready() bool {\n\treturn n.borders[1] != 0\n}\n\n// source returns the source of the current node.\nfunc (n *Node) source() []byte {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified \u0026\u0026 n.data != nil {\n\t\treturn (n.data)[n.borders[0]:n.borders[1]]\n\t}\n\n\treturn nil\n}\n\n// root returns the root node of the current node.\nfunc (n *Node) root() *Node {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tcurr := n\n\tfor curr.prev != nil {\n\t\tcurr = curr.prev\n\t}\n\n\treturn curr\n}\n\n// GetNull returns the null value if current node is null type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"null\"))\n//\tval, err := root.GetNull()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNull returns error: %v\", err)\n//\t}\n//\tif val != nil {\n//\t\tt.Errorf(\"GetNull returns wrong result: %v\", val)\n//\t}\nfunc (n *Node) GetNull() (any, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsNull() {\n\t\treturn nil, errNotNullNode\n\t}\n\n\treturn nil, nil\n}\n\n// MustNull returns the null value if current node is null type.\n//\n// It panics if the current node is not null type.\nfunc (n *Node) MustNull() any {\n\tv, err := n.GetNull()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetNumeric returns the numeric (int/float) value if current node is number type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"10.5\"))\n//\tval, err := root.GetNumeric()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNumeric returns error: %v\", err)\n//\t}\n//\tprintln(val) // 10.5\nfunc (n *Node) GetNumeric() (float64, error) {\n\tif n == nil {\n\t\treturn 0, errNilNode\n\t}\n\n\tif n.nodeType != Number {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tv, ok := val.(float64)\n\tif !ok {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\treturn v, nil\n}\n\n// MustNumeric returns the numeric (int/float) value if current node is number type.\n//\n// It panics if the current node is not number type.\nfunc (n *Node) MustNumeric() float64 {\n\tv, err := n.GetNumeric()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetString returns the string value if current node is string type.\n//\n// Usage:\n//\n//\troot, err := Unmarshal([]byte(\"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n//\t}\n//\n//\tstr, err := root.GetString()\n//\tif err != nil {\n//\t\tt.Errorf(\"should retrieve string value: %s\", err)\n//\t}\n//\n//\tprintln(str) // \"foo\"\nfunc (n *Node) GetString() (string, error) {\n\tif n == nil {\n\t\treturn \"\", errEmptyStringNode\n\t}\n\n\tif !n.IsString() {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tv, ok := val.(string)\n\tif !ok {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\treturn v, nil\n}\n\n// MustString returns the string value if current node is string type.\n//\n// It panics if the current node is not string type.\nfunc (n *Node) MustString() string {\n\tv, err := n.GetString()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetBool returns the boolean value if current node is boolean type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"true\"))\n//\tval, err := root.GetBool()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetBool returns error: %v\", err)\n//\t}\n//\tprintln(val) // true\nfunc (n *Node) GetBool() (bool, error) {\n\tif n == nil {\n\t\treturn false, errNilNode\n\t}\n\n\tif n.nodeType != Boolean {\n\t\treturn false, errNotBoolNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tv, ok := val.(bool)\n\tif !ok {\n\t\treturn false, errNotBoolNode\n\t}\n\n\treturn v, nil\n}\n\n// MustBool returns the boolean value if current node is boolean type.\n//\n// It panics if the current node is not boolean type.\nfunc (n *Node) MustBool() bool {\n\tv, err := n.GetBool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetArray returns the array value if current node is array type.\n//\n// Usage:\n//\n//\t\troot := Must(Unmarshal([]byte(`[\"foo\", 1]`)))\n//\t\tarr, err := root.GetArray()\n//\t\tif err != nil {\n//\t\t\tt.Errorf(\"GetArray returns error: %v\", err)\n//\t\t}\n//\n//\t\tfor _, val := range arr {\n//\t\t\tprintln(val)\n//\t\t}\n//\n//\t result: \"foo\", 1\nfunc (n *Node) GetArray() ([]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.nodeType != Array {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.([]*Node)\n\tif !ok {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\treturn v, nil\n}\n\n// MustArray returns the array value if current node is array type.\n//\n// It panics if the current node is not array type.\nfunc (n *Node) MustArray() []*Node {\n\tv, err := n.GetArray()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendArray appends the given values to the current array node.\n//\n// If the current node is not array type, it returns an error.\n//\n// Example 1:\n//\n//\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n//\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n//\t\tt.Errorf(\"should not return error: %s\", err)\n//\t}\n//\n//\tresult: [{\"foo\":\"bar\"}, null]\n//\n// Example 2:\n//\n//\troot := Must(Unmarshal([]byte(`[\"bar\", \"baz\"]`)))\n//\terr := root.AppendArray(NumberNode(\"\", 1), StringNode(\"\", \"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n//\t }\n//\n//\tresult: [\"bar\", \"baz\", 1, \"foo\"]\nfunc (n *Node) AppendArray(value ...*Node) error {\n\tif !n.IsArray() {\n\t\treturn errInvalidAppend\n\t}\n\n\tfor _, val := range value {\n\t\tif err := n.append(nil, val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ArrayEach executes the callback for each element in the JSON array.\n//\n// Usage:\n//\n//\tjsonArrayNode.ArrayEach(func(i int, valueNode *Node) {\n//\t    ufmt.Println(i, valueNode)\n//\t})\nfunc (n *Node) ArrayEach(callback func(i int, target *Node)) {\n\tif n == nil || !n.IsArray() {\n\t\treturn\n\t}\n\n\tfor idx := 0; idx \u003c len(n.next); idx++ {\n\t\telement, err := n.GetIndex(idx)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcallback(idx, element)\n\t}\n}\n\n// GetObject returns the object value if current node is object type.\n//\n// Usage:\n//\n//\troot := Must(Unmarshal([]byte(`{\"key\": \"value\"}`)))\n//\tobj, err := root.GetObject()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetObject returns error: %v\", err)\n//\t}\n//\n//\tresult: map[string]*Node{\"key\": StringNode(\"key\", \"value\")}\nfunc (n *Node) GetObject() (map[string]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsObject() {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.(map[string]*Node)\n\tif !ok {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\treturn v, nil\n}\n\n// MustObject returns the object value if current node is object type.\n//\n// It panics if the current node is not object type.\nfunc (n *Node) MustObject() map[string]*Node {\n\tv, err := n.GetObject()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendObject appends the given key and value to the current object node.\n//\n// If the current node is not object type, it returns an error.\nfunc (n *Node) AppendObject(key string, value *Node) error {\n\tif !n.IsObject() {\n\t\treturn errInvalidAppend\n\t}\n\n\tif err := n.append(\u0026key, value); err != nil {\n\t\treturn err\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ObjectEach executes the callback for each key-value pair in the JSON object.\n//\n// Usage:\n//\n//\tjsonObjectNode.ObjectEach(func(key string, valueNode *Node) {\n//\t    ufmt.Println(key, valueNode)\n//\t})\nfunc (n *Node) ObjectEach(callback func(key string, value *Node)) {\n\tif n == nil || !n.IsObject() {\n\t\treturn\n\t}\n\n\tfor key, child := range n.next {\n\t\tcallback(key, child)\n\t}\n}\n\n// String converts the node to a string representation.\nfunc (n *Node) String() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified {\n\t\treturn string(n.source())\n\t}\n\n\tval, err := Marshal(n)\n\tif err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\n\treturn string(val)\n}\n\n// Path builds the path of the current node.\n//\n// For example:\n//\n//\t{ \"key\": { \"sub\": [ \"val1\", \"val2\" ] }}\n//\n// The path of \"val2\" is: $.key.sub[1]\nfunc (n *Node) Path() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tif n.prev == nil {\n\t\tsb.WriteString(\"$\")\n\t} else {\n\t\tsb.WriteString(n.prev.Path())\n\n\t\tif n.key != nil {\n\t\t\tsb.WriteString(\"['\" + n.Key() + \"']\")\n\t\t} else {\n\t\t\tsb.WriteString(\"[\" + strconv.Itoa(n.Index()) + \"]\")\n\t\t}\n\t}\n\n\treturn sb.String()\n}\n\n// mark marks the current node as modified.\nfunc (n *Node) mark() {\n\tnode := n\n\tfor node != nil \u0026\u0026 !node.modified {\n\t\tnode.modified = true\n\t\tnode = node.prev\n\t}\n}\n\n// isContainer checks the current node type is array or object.\nfunc (n *Node) isContainer() bool {\n\treturn n.IsArray() || n.IsObject()\n}\n\n// remove removes the value from the current container type node.\nfunc (n *Node) remove(v *Node) error {\n\tif !n.isContainer() {\n\t\treturn ufmt.Errorf(\n\t\t\t\"can't remove value from non-array or non-object node. got=%s\",\n\t\t\tn.Type().String(),\n\t\t)\n\t}\n\n\tif v.prev != n {\n\t\treturn errors.New(\"invalid parent node\")\n\t}\n\n\tn.mark()\n\tif n.IsArray() {\n\t\tdelete(n.next, strconv.Itoa(*v.index))\n\t\tn.dropIndex(*v.index)\n\t} else {\n\t\tdelete(n.next, *v.key)\n\t}\n\n\tv.prev = nil\n\treturn nil\n}\n\n// dropIndex rebase the index of current array node values.\nfunc (n *Node) dropIndex(idx int) {\n\tfor i := idx + 1; i \u003c= len(n.next); i++ {\n\t\tprv := i - 1\n\t\tif curr, ok := n.next[strconv.Itoa(i)]; ok {\n\t\t\tcurr.index = \u0026prv\n\t\t\tn.next[strconv.Itoa(prv)] = curr\n\t\t}\n\n\t\tdelete(n.next, strconv.Itoa(i))\n\t}\n}\n\n// append is a helper function to append the given value to the current container type node.\nfunc (n *Node) append(key *string, val *Node) error {\n\tif n.isSameOrParentNode(val) {\n\t\treturn errInvalidAppendCycle\n\t}\n\n\tif val.prev != nil {\n\t\tif err := val.prev.remove(val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tval.prev = n\n\tval.key = key\n\n\tif key == nil {\n\t\tsize := len(n.next)\n\t\tval.index = \u0026size\n\t\tn.next[strconv.Itoa(size)] = val\n\t} else {\n\t\tif old, ok := n.next[*key]; ok {\n\t\t\tif err := n.remove(old); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tn.next[*key] = val\n\t}\n\n\treturn nil\n}\n\nfunc (n *Node) isSameOrParentNode(nd *Node) bool {\n\treturn n == nd || n.isParentNode(nd)\n}\n\nfunc (n *Node) isParentNode(nd *Node) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\tfor curr := nd.prev; curr != nil; curr = curr.prev {\n\t\tif curr == n {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// cptrs returns the pointer of the given string value.\nfunc cptrs(cpy *string) *string {\n\tif cpy == nil {\n\t\treturn nil\n\t}\n\n\tval := *cpy\n\n\treturn \u0026val\n}\n\n// cptri returns the pointer of the given integer value.\nfunc cptri(i *int) *int {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\tval := *i\n\treturn \u0026val\n}\n\n// Must panics if the given node is not fulfilled the expectation.\n// Usage:\n//\n//\tnode := Must(Unmarshal([]byte(`{\"key\": \"value\"}`))\nfunc Must(root *Node, expect error) *Node {\n\tif expect != nil {\n\t\tpanic(expect)\n\t}\n\n\treturn root\n}\n"},{"name":"parser.gno","body":"package json\n\nimport (\n\t\"bytes\"\n)\n\nconst (\n\tunescapeStackBufSize = 64\n\tabsMinInt64          = 1 \u003c\u003c 63\n\tmaxInt64             = absMinInt64 - 1\n\tmaxUint64            = 1\u003c\u003c64 - 1\n)\n\n// PaseStringLiteral parses a string from the given byte slice.\nfunc ParseStringLiteral(data []byte) (string, error) {\n\tvar buf [unescapeStackBufSize]byte\n\n\tbf, err := Unescape(data, buf[:])\n\tif err != nil {\n\t\treturn \"\", errInvalidStringInput\n\t}\n\n\treturn string(bf), nil\n}\n\n// ParseBoolLiteral parses a boolean value from the given byte slice.\nfunc ParseBoolLiteral(data []byte) (bool, error) {\n\tswitch {\n\tcase bytes.Equal(data, trueLiteral):\n\t\treturn true, nil\n\tcase bytes.Equal(data, falseLiteral):\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, errMalformedBooleanValue\n\t}\n}\n"},{"name":"path.gno","body":"package json\n\nimport (\n\t\"errors\"\n)\n\n// ParsePath takes a JSONPath string and returns a slice of strings representing the path segments.\nfunc ParsePath(path string) ([]string, error) {\n\tbuf := newBuffer([]byte(path))\n\tresult := make([]string, 0)\n\n\tfor {\n\t\tb, err := buf.current()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch {\n\t\tcase b == dollarSign || b == atSign:\n\t\t\tresult = append(result, string(b))\n\t\t\tbuf.step()\n\n\t\tcase b == dot:\n\t\t\tbuf.step()\n\n\t\t\tif next, _ := buf.current(); next == dot {\n\t\t\t\tbuf.step()\n\t\t\t\tresult = append(result, \"..\")\n\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t} else {\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t}\n\n\t\tcase b == bracketOpen:\n\t\t\tstart := buf.index\n\t\t\tbuf.step()\n\n\t\t\tfor {\n\t\t\t\tif buf.index \u003e= buf.length || buf.data[buf.index] == bracketClose {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tbuf.step()\n\t\t\t}\n\n\t\t\tif buf.index \u003e= buf.length {\n\t\t\t\treturn nil, errors.New(\"unexpected end of path\")\n\t\t\t}\n\n\t\t\tsegment := string(buf.sliceFromIndices(start+1, buf.index))\n\t\t\tresult = append(result, segment)\n\n\t\t\tbuf.step()\n\n\t\tdefault:\n\t\t\tbuf.step()\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// extractNextSegment extracts the segment from the current index\n// to the next significant character and adds it to the resulting slice.\nfunc extractNextSegment(buf *buffer, result *[]string) {\n\tstart := buf.index\n\tbuf.skipToNextSignificantToken()\n\n\tif buf.index \u003c= start {\n\t\treturn\n\t}\n\n\tsegment := string(buf.sliceFromIndices(start, buf.index))\n\tif segment != \"\" {\n\t\t*result = append(*result, segment)\n\t}\n}\n"},{"name":"token.gno","body":"package json\n\nconst (\n\tbracketOpen    = '['\n\tbracketClose   = ']'\n\tparenOpen      = '('\n\tparenClose     = ')'\n\tcurlyOpen      = '{'\n\tcurlyClose     = '}'\n\tcomma          = ','\n\tdot            = '.'\n\tcolon          = ':'\n\tbackTick       = '`'\n\tsingleQuote    = '\\''\n\tdoubleQuote    = '\"'\n\temptyString    = \"\"\n\twhiteSpace     = ' '\n\tplus           = '+'\n\tminus          = '-'\n\taesterisk      = '*'\n\tbang           = '!'\n\tquestion       = '?'\n\tnewLine        = '\\n'\n\ttab            = '\\t'\n\tcarriageReturn = '\\r'\n\tformFeed       = '\\f'\n\tbackSpace      = '\\b'\n\tslash          = '/'\n\tbackSlash      = '\\\\'\n\tunderScore     = '_'\n\tdollarSign     = '$'\n\tatSign         = '@'\n\tandSign        = '\u0026'\n\torSign         = '|'\n)\n\nvar (\n\ttrueLiteral  = []byte(\"true\")\n\tfalseLiteral = []byte(\"false\")\n\tnullLiteral  = []byte(\"null\")\n)\n\ntype ValueType int\n\nconst (\n\tNotExist ValueType = iota\n\tString\n\tNumber\n\tFloat\n\tObject\n\tArray\n\tBoolean\n\tNull\n\tUnknown\n)\n\nfunc (v ValueType) String() string {\n\tswitch v {\n\tcase NotExist:\n\t\treturn \"not-exist\"\n\tcase String:\n\t\treturn \"string\"\n\tcase Number:\n\t\treturn \"number\"\n\tcase Object:\n\t\treturn \"object\"\n\tcase Array:\n\t\treturn \"array\"\n\tcase Boolean:\n\t\treturn \"boolean\"\n\tcase Null:\n\t\treturn \"null\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"test","path":"gno.land/r/gov/dao/v3/treasury/test","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/gov/dao/v3/treasury/test\"\ngno = \"0.9\"\n"},{"name":"workaround.gno","body":"package test\n\n// This package exists solely to circumvent a limitation associated with the\n// suffixed test package (a test package sharing the same folder as the main\n// package to be tested but having the suffix _test in its name).\n// Currently, the GnoVM no longer differentiates between the dependencies of a\n// package and its test package, which causes circular dependencies issues.\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"dom","path":"gno.land/p/archive/dom","files":[{"name":"dom.gno","body":"// XXX This is only used for testing in ./tests.\n// Otherwise this package is deprecated.\n// TODO: replace with a package that is supported, and delete this.\n\npackage dom\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype Plot struct {\n\tName     string\n\tPosts    avl.Tree // postsCtr -\u003e *Post\n\tPostsCtr int\n}\n\nfunc (plot *Plot) AddPost(title string, body string) {\n\tctr := plot.PostsCtr\n\tplot.PostsCtr++\n\tkey := strconv.Itoa(ctr)\n\tpost := \u0026Post{\n\t\tTitle: title,\n\t\tBody:  body,\n\t}\n\tplot.Posts.Set(key, post)\n}\n\nfunc (plot *Plot) String() string {\n\tstr := \"# [plot] \" + plot.Name + \"\\n\"\n\tif plot.Posts.Size() \u003e 0 {\n\t\tplot.Posts.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Post).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Post struct {\n\tTitle    string\n\tBody     string\n\tComments avl.Tree\n}\n\nfunc (post *Post) String() string {\n\tstr := \"## \" + post.Title + \"\\n\"\n\tstr += \"\"\n\tstr += post.Body\n\tif post.Comments.Size() \u003e 0 {\n\t\tpost.Comments.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Comment).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Comment struct {\n\tCreator string\n\tBody    string\n}\n\nfunc (cmm Comment) String() string {\n\treturn cmm.Body + \" - @\" + cmm.Creator + \"\\n\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/archive/dom\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"commondao","path":"gno.land/p/nt/commondao/v0","files":[{"name":"README.md","body":"\u003e **v0 - Unaudited**\n\u003e This is an initial version of this package that has not yet been formally audited.\n\u003e A fully audited version will be published as a subsequent release.\n\u003e Use in production at your own risk.\n\n# commondao\n\nGovernance primitives following the Common DAO Spec\n(`docs/CONSTITUTION.md`, Appendix): a `CommonDAO` is a **Council** (a\nset of addresses with equal voting power), a proposal lifecycle, and an\noptional sub-DAO tree.\n\n```\nCommonDAO\n├── council:            *addrset.Set — who may vote\n├── kinds:              registered ProposalKind factories — what may be\n│                       proposed (name → New(readonly dao, args))\n├── active proposals:   active + early passed, each with an electorate\n│                       snapshot and voting record\n├── finished proposals: dismissed / executed / failed / withdrawn\n├── treasury:           a derived address + frozen flag (funds moved by\n│                       the hosting realm, never by this package)\n└── children:           sub-DAOs (each a CommonDAO with a parent pointer)\n```\n\n## Quick start\n\n```go\nimport \"gno.land/p/nt/commondao/v0\"\n\n// A proposal kind names one proposal type and builds its definitions.\ntype textKind struct{}\n\nfunc (textKind) Name() string { return \"text\" }\nfunc (textKind) New(dao commondao.ReadonlyCommonDAO, args any) (commondao.ProposalDefinition, error) {\n    text, ok := args.(string) // validate args, build the definition\n    if !ok || text == \"\" {\n        return nil, errors.New(\"a proposal text is required\")\n    }\n    return textDefinition{text}, nil\n}\n\nvar dao = commondao.New(\n    commondao.WithName(\"My DAO\"),\n    commondao.WithCouncilMember(founder),\n    commondao.WithProposalKind(textKind{}), // a type is proposable iff registered\n)\n\n// Propose looks the kind up in the DAO's registry and calls its New\n// factory with a readonly view of the host DAO and args to build the\n// frozen definition. The council snapshot taken at Propose is the\n// proposal's electorate. (The package does not gate who proposes —\n// hosting realms do.)\np, _ := dao.Propose(founder, \"text\", \"hello world\")\n\n// Electorate members vote; default rule proposals can be decided the\n// moment the outcome is settled.\ndao.Vote(founder, p.ID(), commondao.ChoiceYes, \"\")\n\n// Execute runs passed proposals (early passed ones immediately, active\n// ones once their voting deadline passes). The host mints a DAO-scoped\n// sub-identity and passes it as the executor's value-movement authority.\ndao.Execute(p.ID(), sub)\n```\n\n## Voting rules (the constitutional defaults)\n\nEvery proposal definition returns a `Threshold()`; proposals are decided\nby `TallyDefault` with integer math over the proposal's **electorate\nsnapshot** E (the council at `Propose` time):\n\n```\nD = |E| - abstains                 // the tally denominator\nsupermajority:   pass ⇔ D \u003e 0 \u0026\u0026 3*yes \u003e= 2*D   (\"two thirds or more\")\nsimple majority: pass ⇔ D \u003e 0 \u0026\u0026 2*yes \u003e D       (\"more than half\")\ndismiss (both):       ⇔ 2*no \u003e D\nundecided at deadline ⇒ dismissed\n```\n\nAbstaining shrinks the denominator (deference); not voting counts\nagainst passage (silence is opposition). Votes are re-evaluated after\nevery ballot — including changed votes — so a proposal **passes or is\ndismissed the moment the outcome is mathematically settled** and an\nearly-passed proposal may be executed before its deadline.\n\nVote choices are fixed at YES/NO/ABSTAIN.\n\n## Council changes\n\n`UpdateCouncil(add, remove)` applies idempotent set operations: the\nfinal set is `(council ∪ add) \\ remove`, duplicate adds and absent\nremoves are no-ops (so concurrently passed updates merge in execution\norder), full replacement in one call is legal, and an update that\nwould empty a non-empty council returns `ErrEmptyCouncil` — executors\npropagate the error to fail the proposal cleanly.\n\n## Proposal kinds\n\nProposal types are registered on the DAO, not passed per proposal: a\n`ProposalKind` couples a registry name with a\n`New(dao ReadonlyCommonDAO, args)` factory, and\n`Propose(creator, kind, args)` accepts exactly the kinds registered\n(`WithProposalKind` at construction; `RegisterKind`/`DeregisterKind`\nafterwards, typically from a governance proposal executor). The registry\nis read only at `Propose`: deregistering a kind blocks new proposals but\nnever touches in-flight ones, whose definitions were frozen at creation.\n`HasKind`/`KindNames` expose the registry, also on the readonly view.\n\n`New` receives only a **`ReadonlyCommonDAO`**, so a kind — including an\nexternally-authored or user-registered one — cannot mutate the host DAO\n(or its tree) at `Propose` time, before the vote. A kind that must mutate\nstate on execution takes the target `*CommonDAO` through `args`, which\nonly a trusted caller can populate (an external proposer cannot obtain a\n`*CommonDAO`), captures it in the definition, and mutates in its\n`Executor` — which runs only after the vote passes.\n\n### The `ExecutionKind` concrete kind\n\nThe package ships exactly one concrete kind, `/p/`-typed so any realm can\nseed it with `WithProposalKind(ExecutionKind{})` or register it later\nwith `RegisterKind`:\n\n- **`ExecutionKind`** (`\"execution\"`) runs an arbitrary `ExecFunc`\n  supplied by the proposer (`ExecutionArgs{Title, Body, Fn}`) on\n  approval, under a default policy (7-day voting period, supermajority\n  threshold) and no check on the closure beyond a non-nil `Fn`. The `Fn`\n  closure is frozen at `Propose` (vote-integrity), so it **must be\n  authored in a persistent realm** — a closure created by a `maketx run`\n  script does not persist to `Execute` and cannot run.\n\n  Because it applies no policy to the closure, a realm with treasury\n  constraints (e.g. a freeze flag) should **not** catalog `ExecutionKind`\n  directly: it should author its own execution kind whose definition wraps\n  the closure with a `Validable` check enforcing those constraints, so\n  arbitrary execution cannot bypass them. The reference realm does this to\n  keep a frozen DAO from draining its own treasury via an execution\n  proposal.\n\nA registered foreign-realm kind runs under its **defining** realm's\nauthority — registering one is a governance trust grant, not a sandbox.\n\nThe package ships **no** governance meta-kinds. `RegisterKind` /\n`DeregisterKind` are plain registry primitives with no reserved names:\nany registered kind can be removed. Managing a DAO's kind set through\ngovernance — and keeping a managing kind un-removable so a DAO can always\nrecover — is the consuming realm's policy, built on these primitives (see\nthe reference realm's `manage-kinds` kind).\n\n## Extending commondao in your own realm\n\nThe package is mostly mechanism: it ships the `ExecutionKind` concrete\nkind (with a default voting policy) and the registry primitives, and\nleaves the rest of governance policy — which kinds a DAO accepts, how it\nmanages them, and any per-kind constraints such as a treasury freeze — to\nthe consuming realm. To add your own proposal type:\n\n1. **Author a `ProposalKind`** — `Name()` plus\n   `New(dao ReadonlyCommonDAO, args any) (ProposalDefinition, error)`. Make\n   the definition `Executable` if it mutates on approval. If its executor\n   moves funds from a DAO other than the host, have the **host** realm\n   consume a `Funded`-style contract (`FundingDAOID() uint64`): minting a\n   DAO sub needs the host's `cur`, so it is host-consumed, not\n   package-dispatched — define it in your realm.\n2. **Seed it** — the owning realm holds the handle, so no proposal is\n   needed: `commondao.New(WithProposalKind(YourKind{}), …)` at\n   construction, or `dao.RegisterKind(YourKind{})` directly.\n3. **Author a typed, CLI-friendly wrapper**\n   `CreateYourProposal(cur realm, daoID uint64, …params…)` that\n   council-gates the caller, builds the args, and calls `Propose`.\n4. **Optionally add a governance toggle** — a `manage-kinds`-style kind\n   whose executor calls `RegisterKind`/`DeregisterKind`, kept itself\n   un-deregisterable, if the council should manage kinds at runtime.\n\n**Trust boundary:** `New` gets only a `ReadonlyCommonDAO`; the mutable\n`*CommonDAO` reaches a definition only via args your trusted wrapper\npopulates; the executor gets the DAO's terminal, RealmSend-only sub. See\nthe reference realm for a worked example.\n\n## Proposal lifecycle\n\n`Propose` (kind-gated as above; capped via `SetMaxActiveProposals`;\n`CapExempt` definitions such as council updates bypass the cap, bounded\nto one active proposal per creator) → `Vote` (electorate-gated,\ndeadline-gated, rejects non-active proposals) → `Execute`\n(early-passed: immediately, still validating; active: after the\ndeadline, dismissing undecided proposals) or `Withdraw` (active, zero\nvotes). `Dissolve` dismisses every in-flight proposal and soft deletes\nthe DAO; deleted DAOs reject proposals, votes, and executions.\n\n## Treasury\n\nThe package stores a treasury `address` (`WithAddress`, `Address()`) and\na frozen flag (`SetTreasuryFrozen`, `IsTreasuryFrozen`) but never moves\nfunds — hosting realms derive the address (typically a realm\nsub-identity via `chain.DerivePkgSubAddr`) and enforce the frozen flag.\n`Execute` runs the executor with the DAO-scoped sub-identity the host\npasses as its value-movement authority: a fund-moving definition builds\nits banker from that `sub`, so value moves are structurally bounded to\nthat one DAO address. Which DAO's sub the host mints is the host's\ndecision — minting a sub needs the host realm's `cur`, so the package\ncannot make it — typically the proposal's own DAO, but a fund-moving\ndefinition may direct the host to a different DAO (e.g. clawback sweeps\nthe target, not the host). See the reference realm's treasury proposals\nand its host-side `Funded` contract for the constitutional pattern.\n\n## Realm boundaries\n\nA `*CommonDAO` is a mutable handle for the realm that owns it:\n\n1. Do not ACCEPT a `*CommonDAO` from an untrusted caller.\n2. Do not RETURN a `*CommonDAO` — return `dao.Readonly()`, a\n   `ReadonlyCommonDAO` view whose whole reachable graph is read-only\n   (`ReadonlyProposal` flattens `Title()`/`Body()` and never exposes\n   the `ProposalDefinition`, whose executor would otherwise be\n   callable under your realm's authority).\n3. Do not TRUST a readonly view received from an untrusted caller —\n   it is a live handle over the sender's data.\n\nSee `gno.land/r/nt/commondao/v0` for the reference realm hosting many\nDAOs with invitations, council governance, treasuries, and rendering.\n"},{"name":"commondao.gno","body":"package commondao\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/addrset/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/bptree/v0/list\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// DefaultMaxActiveProposals is the default cap for simultaneously active\n// proposals per DAO. Every active proposal stores a council snapshot, so\n// the cap bounds storage. It is applied at construction; a hosting realm\n// may override it per DAO via SetMaxActiveProposals (the reference realm\n// keeps this default).\nconst DefaultMaxActiveProposals = 32\n\nvar (\n\tErrCouncilUpdateOverlap  = errors.New(\"council update adds and removes the same address\")\n\tErrDAOIsDeleted          = errors.New(\"DAO is deleted\")\n\tErrEmptyCouncil          = errors.New(\"council update would remove every council member\")\n\tErrExecutionNotAllowed   = errors.New(\"proposal must be active or passed to be executed\")\n\tErrInvalidVoteChoice     = errors.New(\"invalid vote choice\")\n\tErrMaxActiveProposals    = errors.New(\"max number of active proposals reached\")\n\tErrMaxCapExemptProposals = errors.New(\"creator already has an active cap exempt proposal\")\n\tErrNotElectorateMember   = errors.New(\"account is not a member of the proposal's electorate\")\n\tErrOverflow              = errors.New(\"next ID overflows uint64\")\n\tErrProposalKindExists    = errors.New(\"proposal kind already registered\")\n\tErrProposalKindNotFound  = errors.New(\"proposal kind not found\")\n\tErrProposalKindRequired  = errors.New(\"proposal kind is required\")\n\tErrProposalNotFound      = errors.New(\"proposal not found\")\n\tErrVotingDeadlineNotMet  = errors.New(\"voting deadline not met\")\n\tErrVotingDeadlinePassed  = errors.New(\"voting deadline has passed\")\n\tErrWithdrawalNotAllowed  = errors.New(\"withdrawal not allowed for proposals with votes\")\n)\n\n// CommonDAO defines a DAO.\n//\n// # Security\n//\n// A *CommonDAO is a mutable handle: its exported mutators (UpdateCouncil,\n// Dissolve, Propose, Vote, Execute, Withdraw, SetTreasuryFrozen,\n// SetMaxActiveProposals, RegisterKind, DeregisterKind) are meant for the\n// realm that owns the DAO.\n// Three rules apply at realm boundaries:\n//\n//  1. Do not ACCEPT a *CommonDAO from an external/untrusted caller.\n//  2. Do not RETURN a *CommonDAO from any function callable by untrusted\n//     realms — return dao.Readonly() (a ReadonlyCommonDAO view) instead.\n//  3. Do not TRUST a readonly view received from an untrusted caller: it\n//     is a live handle over the sender's data.\ntype CommonDAO struct {\n\tid                 uint64\n\tname               string\n\tdescription        string\n\tpurpose            string\n\taddr               address // derived treasury address, empty when unset\n\tparent             *CommonDAO\n\tchildren           list.IList\n\tcouncil            *addrset.Set\n\tgenID              seqid.ID\n\tkinds              *bptree.BPTree // proposal kind name -\u003e ProposalKind\n\tactiveProposals    *proposalStorage\n\tfinishedProposals  *proposalStorage\n\tdeleted            bool // Soft delete\n\ttreasuryFrozen     bool\n\tmaxActiveProposals int\n\tproposing          bool // re-entrancy latch around a kind's New in Propose\n\texecuting          bool // re-entrancy latch around Execute\n}\n\n// New creates a new common DAO.\nfunc New(options ...Option) *CommonDAO {\n\tdao := \u0026CommonDAO{\n\t\tchildren:           \u0026list.List{},\n\t\tcouncil:            \u0026addrset.Set{},\n\t\tkinds:              bptree.NewBPTree32(),\n\t\tactiveProposals:    newProposalStorage(),\n\t\tfinishedProposals:  newProposalStorage(),\n\t\tmaxActiveProposals: DefaultMaxActiveProposals,\n\t}\n\tfor _, apply := range options {\n\t\tapply(dao)\n\t}\n\treturn dao\n}\n\n// ID returns DAO's unique identifier.\nfunc (dao CommonDAO) ID() uint64 {\n\treturn dao.id\n}\n\n// Name returns DAO's name.\nfunc (dao CommonDAO) Name() string {\n\treturn dao.name\n}\n\n// Purpose returns the DAO's purpose. Together with the description it\n// forms the DAO's Charter (docs/CONSTITUTION.md :1485).\nfunc (dao CommonDAO) Purpose() string {\n\treturn dao.purpose\n}\n\n// Description returns DAO's description.\nfunc (dao CommonDAO) Description() string {\n\treturn dao.description\n}\n\n// Address returns the DAO's treasury address, assigned at creation with\n// WithAddress. The package never derives or uses the address itself:\n// hosting realms derive it (e.g. from a realm sub-identity) and operate\n// its funds through their own banker. Empty when unset.\nfunc (dao CommonDAO) Address() address {\n\treturn dao.addr\n}\n\n// IsTreasuryFrozen checks if the DAO's treasury is frozen. The package\n// stores the flag only; hosting realms enforce it when moving funds.\nfunc (dao CommonDAO) IsTreasuryFrozen() bool {\n\treturn dao.treasuryFrozen\n}\n\n// SetTreasuryFrozen freezes or unfreezes the DAO's treasury.\nfunc (dao *CommonDAO) SetTreasuryFrozen(frozen bool) {\n\tdao.treasuryFrozen = frozen\n}\n\n// Parent returns the parent DAO.\n// Null can be returned when DAO has no parent assigned.\nfunc (dao CommonDAO) Parent() *CommonDAO {\n\treturn dao.parent\n}\n\n// ChildrenCount returns the number of direct children DAOs.\nfunc (dao CommonDAO) ChildrenCount() int {\n\treturn dao.children.Len()\n}\n\n// IterateChildren iterates the direct children DAOs.\nfunc (dao CommonDAO) IterateChildren(fn func(*CommonDAO) bool) (stopped bool) {\n\tdao.children.ForEach(func(_ int, v any) bool {\n\t\tstopped = fn(v.(*CommonDAO))\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// Council returns a read only view of the DAO council.\n//\n// The council is the set of addresses entitled to vote. It changes only\n// through UpdateCouncil (normally called by a council update proposal\n// executor) or constructor options.\nfunc (dao CommonDAO) Council() *addrset.ReadonlySet {\n\treturn dao.council.Readonly()\n}\n\n// UpdateCouncil adds and removes council members as idempotent set\n// operations: adding an existing member or removing an absent one is a\n// no-op, so concurrently passed council updates merge deterministically in\n// execution order, and a full council replacement in a single update is\n// legal.\n//\n// The final set is (council ∪ add) \\ remove. An update that adds and\n// removes the same address is rejected, and an update whose final set\n// would empty a non-empty council returns ErrEmptyCouncil: executors must\n// propagate the error (failing the proposal) instead of panicking, which\n// would revert the transaction and leave the proposal stuck.\nfunc (dao *CommonDAO) UpdateCouncil(add, remove []address) error {\n\tfor _, a := range add {\n\t\tfor _, r := range remove {\n\t\t\tif a == r {\n\t\t\t\treturn ErrCouncilUpdateOverlap\n\t\t\t}\n\t\t}\n\t}\n\n\t// The final set can only be empty when nothing is added: overlap is\n\t// rejected above, so any added address survives its own update.\n\tif dao.council.Size() \u003e 0 \u0026\u0026 len(add) == 0 {\n\t\tempty := true\n\t\tdao.council.IterateByOffset(0, dao.council.Size(), func(member address) bool {\n\t\t\tfor _, r := range remove {\n\t\t\t\tif r == member {\n\t\t\t\t\treturn false // removed: keep looking for a survivor\n\t\t\t\t}\n\t\t\t}\n\t\t\tempty = false\n\t\t\treturn true\n\t\t})\n\t\tif empty {\n\t\t\treturn ErrEmptyCouncil\n\t\t}\n\t}\n\n\tfor _, a := range add {\n\t\tdao.council.Add(a)\n\t}\n\tfor _, r := range remove {\n\t\tdao.council.Remove(r)\n\t}\n\treturn nil\n}\n\n// ActiveProposalsSize returns the number of active proposals, including\n// early passed proposals that were not executed yet.\nfunc (dao CommonDAO) ActiveProposalsSize() int {\n\treturn dao.activeProposals.Size()\n}\n\n// IterateActiveProposals iterates active proposals ordered by ID.\nfunc (dao CommonDAO) IterateActiveProposals(offset, count int, reverse bool, fn func(*Proposal) bool) bool {\n\treturn dao.activeProposals.Iterate(offset, count, reverse, fn)\n}\n\n// FinishedProposalsSize returns the number of finished proposals.\nfunc (dao CommonDAO) FinishedProposalsSize() int {\n\treturn dao.finishedProposals.Size()\n}\n\n// IterateFinishedProposals iterates finished proposals ordered by ID.\nfunc (dao CommonDAO) IterateFinishedProposals(offset, count int, reverse bool, fn func(*Proposal) bool) bool {\n\treturn dao.finishedProposals.Iterate(offset, count, reverse, fn)\n}\n\n// IsDeleted returns true when DAO has been soft deleted.\nfunc (dao CommonDAO) IsDeleted() bool {\n\treturn dao.deleted\n}\n\n// MaxActiveProposals returns the cap for simultaneously active proposals.\nfunc (dao CommonDAO) MaxActiveProposals() int {\n\treturn dao.maxActiveProposals\n}\n\n// SetMaxActiveProposals changes the cap for simultaneously active\n// proposals. Values below one are ignored: a DAO must always be able to\n// propose.\nfunc (dao *CommonDAO) SetMaxActiveProposals(max int) {\n\tif max \u003e= 1 {\n\t\tdao.maxActiveProposals = max\n\t}\n}\n\n// RegisterKind registers a proposal kind by its name.\n//\n// Registered kinds are the only way to create proposals: Propose looks\n// kinds up by name and calls their New factory, so a proposal type is\n// proposable iff its kind is registered. Like SetTreasuryFrozen, this\n// mutator is meant for the realm that owns the DAO (typically called at\n// DAO creation and by governance proposal executors).\nfunc (dao *CommonDAO) RegisterKind(k ProposalKind) error {\n\tif k == nil || k.Name() == \"\" {\n\t\treturn ErrProposalKindRequired\n\t}\n\tif dao.kinds.Has(k.Name()) {\n\t\treturn ErrProposalKindExists\n\t}\n\tdao.kinds.Set(k.Name(), k)\n\treturn nil\n}\n\n// DeregisterKind removes a proposal kind by name.\n//\n// Deregistering only blocks new proposals: the registry is read at\n// Propose time only, so in-flight proposals of the kind keep their\n// frozen definition and still vote and execute.\n//\n// This is a plain registry primitive with no reserved names: any\n// registered kind can be removed. A consuming realm that must keep a\n// kind un-removable (e.g. a governance kind that manages the kind set)\n// enforces that as its own policy, not through this package.\nfunc (dao *CommonDAO) DeregisterKind(name string) error {\n\tif _, removed := dao.kinds.Remove(name); !removed {\n\t\treturn ErrProposalKindNotFound\n\t}\n\treturn nil\n}\n\n// HasKind checks if a proposal kind is registered.\nfunc (dao CommonDAO) HasKind(name string) bool {\n\treturn dao.kinds.Has(name)\n}\n\n// KindNames returns the names of the registered proposal kinds, sorted.\nfunc (dao CommonDAO) KindNames() []string {\n\tnames := make([]string, 0, dao.kinds.Size())\n\tdao.kinds.IterateByOffset(0, dao.kinds.Size(), func(name string, _ any) bool {\n\t\tnames = append(names, name)\n\t\treturn false\n\t})\n\treturn names\n}\n\n// Propose creates a new DAO proposal.\n//\n// Proposals are created through registered proposal kinds: the kind is\n// looked up by name in the DAO's registry and its New factory builds the\n// proposal definition from args. The registry is read only here and the\n// definition is frozen once the proposal is created, so deregistering a\n// kind later never touches in-flight proposals.\n//\n// The proposal's electorate is the council snapshot taken now: members\n// added later vote on the next proposal; members removed later remain in\n// the electorate, where their silence counts against passage.\n//\n// The number of simultaneously active proposals is capped. Definitions\n// implementing CapExempt (e.g. council updates, which must never be\n// blockable by a full cap) are exempt but bounded to one active proposal\n// per creator.\nfunc (dao *CommonDAO) Propose(creator address, kind string, args any) (*Proposal, error) {\n\tif dao.deleted {\n\t\treturn nil, ErrDAOIsDeleted\n\t}\n\n\tv := dao.kinds.Get(kind)\n\tif v == nil {\n\t\treturn nil, ErrProposalKindNotFound\n\t}\n\n\t// Re-entrancy latch: a kind's New must not trigger another Propose on\n\t// this DAO (e.g. via a captured handle), which could nest factory\n\t// calls or grow active storage unboundedly before the first returns.\n\tif dao.proposing {\n\t\tpanic(\"commondao: re-entrant Propose is not allowed\")\n\t}\n\tdao.proposing = true\n\t// Deferred so a panicking New cannot leave the latch stuck (which would\n\t// brick every future Propose on this DAO for a consumer that recovers\n\t// the panic within the transaction); mirrors the executing latch.\n\tdefer func() { dao.proposing = false }()\n\td, err := v.(ProposalKind).New(dao.Readonly(), args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif d == nil {\n\t\treturn nil, ErrProposalDefinitionRequired\n\t}\n\n\tif _, exempt := d.(CapExempt); exempt {\n\t\tvar found bool\n\t\tdao.activeProposals.Iterate(0, dao.activeProposals.Size(), false, func(p *Proposal) bool {\n\t\t\tif _, ok := p.definition.(CapExempt); ok \u0026\u0026 p.creator == creator {\n\t\t\t\tfound = true\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t\tif found {\n\t\t\treturn nil, ErrMaxCapExemptProposals\n\t\t}\n\t} else if dao.activeProposals.Size() \u003e= dao.maxActiveProposals {\n\t\treturn nil, ErrMaxActiveProposals\n\t}\n\n\tid, ok := dao.genID.TryNext()\n\tif !ok {\n\t\treturn nil, ErrOverflow\n\t}\n\n\tp, err := newProposal(uint64(id), creator, d)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Snapshot the current council as the proposal's electorate\n\tdao.council.IterateByOffset(0, dao.council.Size(), func(member address) bool {\n\t\tp.electorate.Add(member)\n\t\treturn false\n\t})\n\n\tdao.activeProposals.Add(p)\n\treturn p, nil\n}\n\n// GetProposal returns a proposal or nil when proposal is not found.\nfunc (dao CommonDAO) GetProposal(proposalID uint64) *Proposal {\n\tp := dao.activeProposals.Get(proposalID)\n\tif p != nil {\n\t\treturn p\n\t}\n\treturn dao.finishedProposals.Get(proposalID)\n}\n\n// Withdraw withdraws a proposal that has no votes.\n// Only active proposals without votes can be withdrawn, and once\n// withdrawn they are considered finished.\nfunc (dao *CommonDAO) Withdraw(proposalID uint64) error {\n\tp := dao.activeProposals.Get(proposalID)\n\tif p == nil {\n\t\treturn ErrProposalNotFound\n\t}\n\n\tif p.status != StatusActive {\n\t\treturn ErrStatusIsNotActive\n\t}\n\n\tif p.record.Size() \u003e 0 {\n\t\treturn ErrWithdrawalNotAllowed\n\t}\n\n\tp.status = StatusWithdrawn\n\tdao.activeProposals.Remove(p.id)\n\tdao.finishedProposals.Add(p)\n\treturn nil\n}\n\n// Vote submits a new vote for a proposal.\n//\n// Votes are only allowed to members of the proposal's electorate while the\n// proposal is active and within the voting period. A member may change\n// their vote by voting again.\n//\n// Proposals are re-evaluated after every recorded vote: a YES tally at\n// the definition's threshold decides the proposal immediately, and a\n// simple majority of NO dismisses it immediately.\nfunc (dao *CommonDAO) Vote(member address, proposalID uint64, c VoteChoice, reason string) error {\n\tif dao.deleted {\n\t\treturn ErrDAOIsDeleted\n\t}\n\n\tp := dao.activeProposals.Get(proposalID)\n\tif p == nil {\n\t\treturn ErrProposalNotFound\n\t}\n\n\tif p.status != StatusActive {\n\t\treturn ErrStatusIsNotActive\n\t}\n\n\tif !p.electorate.Has(member) {\n\t\treturn ErrNotElectorateMember\n\t}\n\n\tif p.HasVotingDeadlinePassed() {\n\t\treturn ErrVotingDeadlinePassed\n\t}\n\n\tif c != ChoiceYes \u0026\u0026 c != ChoiceNo \u0026\u0026 c != ChoiceAbstain {\n\t\treturn ErrInvalidVoteChoice\n\t}\n\n\tp.record.AddVote(Vote{\n\t\taddr:   member,\n\t\tchoice: c,\n\t\treason: reason,\n\t})\n\n\t// Early termination: proposals are decided the moment the outcome is\n\t// mathematically settled. A passed proposal stays in the active\n\t// storage until executed; a dismissed one is finished.\n\tswitch TallyDefault(p.record.Readonly(), p.Electorate(), p.definition.Threshold()) {\n\tcase OutcomePassed:\n\t\tp.status = StatusPassed\n\tcase OutcomeDismissed:\n\t\tdao.dismiss(p)\n\t}\n\treturn nil\n}\n\n// Execute executes a proposal.\n//\n// Proposals that already passed (decided early by the default Council\n// rules) execute immediately. Active proposals are tallied once their\n// voting deadline passes and are dismissed unless passed.\n//\n// sub is the DAO-scoped sub-identity that the host mints and passes into\n// the executor as its value-movement authority (see ExecFunc). The\n// executor is non-crossing, so it is called directly. Execute itself is\n// not a crossing function (sub sits in a non-first parameter slot)\n// because /p/ production code cannot declare crossing functions.\nfunc (dao *CommonDAO) Execute(proposalID uint64, sub realm) error {\n\tif dao.deleted {\n\t\treturn ErrDAOIsDeleted\n\t}\n\n\t// Re-entrancy latch: an executor must not re-enter Execute on this\n\t// DAO. Remove-before-run already stops the same proposal from running\n\t// twice; this additionally blocks an executor from executing a\n\t// different proposal of the same DAO mid-execution.\n\tif dao.executing {\n\t\tpanic(\"commondao: re-entrant Execute is not allowed\")\n\t}\n\tdao.executing = true\n\tdefer func() { dao.executing = false }()\n\n\tp := dao.activeProposals.Get(proposalID)\n\tif p == nil {\n\t\treturn ErrProposalNotFound\n\t}\n\n\tswitch p.status {\n\tcase StatusPassed:\n\t\t// Decided early: execute now, before the voting deadline\n\tcase StatusActive:\n\t\tif !p.HasVotingDeadlinePassed() {\n\t\t\treturn ErrVotingDeadlineNotMet\n\t\t}\n\tdefault:\n\t\treturn ErrExecutionNotAllowed\n\t}\n\n\t// The proposal leaves active storage before any definition code\n\t// (Validate, the executor) runs, so a re-entrant Execute call\n\t// cannot run it twice.\n\tdao.activeProposals.Remove(p.id)\n\n\t// Tally proposals that are still active after their deadline;\n\t// undecided proposals are dismissed. Vote already decides settled\n\t// outcomes, so this re-tally only matters for definitions whose\n\t// Threshold is not constant.\n\tif p.status == StatusActive {\n\t\tif TallyDefault(p.record.Readonly(), p.Electorate(), p.definition.Threshold()) == OutcomePassed {\n\t\t\tp.status = StatusPassed\n\t\t} else {\n\t\t\tp.status = StatusDismissed\n\t\t\tdao.finishedProposals.Add(p)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// IMPORTANT, from this point on, any error is going to result\n\t// in a proposal failure and execute will succeed.\n\n\t// Validate the passed proposal before execution\n\terr := p.Validate()\n\n\t// Execute proposal only if it's executable\n\tif err == nil {\n\t\tif e, ok := p.Definition().(Executable); ok {\n\t\t\tif fn := e.Executor(); fn != nil {\n\t\t\t\terr = fn(0, sub)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Proposal fails if there is any error during validation and execution process\n\tif err != nil {\n\t\tp.status = StatusFailed\n\t\tp.statusReason = err.Error()\n\t} else {\n\t\tp.status = StatusExecuted\n\t\tp.statusReason = \"\"\n\t}\n\n\t// Whichever the outcome of the validation, tallying\n\t// and execution consider the proposal finished.\n\tdao.finishedProposals.Add(p)\n\treturn nil\n}\n\n// dismiss finishes a proposal as dismissed.\nfunc (dao *CommonDAO) dismiss(p *Proposal) {\n\tp.status = StatusDismissed\n\tdao.activeProposals.Remove(p.id)\n\tdao.finishedProposals.Add(p)\n}\n\n// Dissolve soft deletes the DAO after dismissing every in-flight proposal\n// (both still-active and passed-but-unexecuted ones). Dissolution is\n// terminal: a deleted DAO rejects proposals, votes and executions, so\n// nothing may remain pending.\nfunc (dao *CommonDAO) Dissolve(reason string) {\n\tvar pending []*Proposal\n\tdao.activeProposals.Iterate(0, dao.activeProposals.Size(), false, func(p *Proposal) bool {\n\t\tpending = append(pending, p)\n\t\treturn false\n\t})\n\n\tfor _, p := range pending {\n\t\tp.statusReason = reason\n\t\tdao.dismiss(p)\n\t}\n\tdao.deleted = true\n}\n"},{"name":"commondao_options.gno","body":"package commondao\n\n// Option configures the CommonDAO.\ntype Option func(*CommonDAO)\n\n// WithID assigns a unique identifier to the DAO.\nfunc WithID(id uint64) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.id = id\n\t}\n}\n\n// WithName assigns a name to the DAO.\nfunc WithName(name string) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.name = name\n\t}\n}\n\n// WithPurpose assigns a purpose to the DAO. Purpose and description\n// together form the DAO's Charter (docs/CONSTITUTION.md :1485).\nfunc WithPurpose(purpose string) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.purpose = purpose\n\t}\n}\n\n// WithDescription assigns a description to the DAO.\nfunc WithDescription(description string) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.description = description\n\t}\n}\n\n// WithAddress assigns a treasury address to the DAO. Hosting realms\n// derive it, typically as a realm sub-identity address\n// (chain.DerivePkgSubAddr) so each DAO owns a distinct account.\nfunc WithAddress(addr address) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.addr = addr\n\t}\n}\n\n// WithParent assigns a parent DAO and registers the DAO as one of the\n// parent's children, keeping both sides of the tree wired in one step.\nfunc WithParent(p *CommonDAO) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.parent = p\n\t\tif p != nil {\n\t\t\tp.children.Append(dao)\n\t\t}\n\t}\n}\n\n// WithCouncilMember assigns a council member to the DAO.\nfunc WithCouncilMember(addr address) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.council.Add(addr)\n\t}\n}\n\n// WithProposalKind registers a proposal kind on the DAO.\n// It panics when the kind is nil, has an empty name, or its name is\n// already registered.\n//\n// The package ships one concrete kind, ExecutionKind; seed it with\n// WithProposalKind(ExecutionKind{}).\nfunc WithProposalKind(k ProposalKind) Option {\n\treturn func(dao *CommonDAO) {\n\t\tif err := dao.RegisterKind(k); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n"},{"name":"doc.gno","body":"// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package commondao provides governance primitives following the Common\n// DAO Spec (docs/CONSTITUTION.md, Appendix): a CommonDAO is a Council (a\n// set of addresses with equal voting power), a proposal lifecycle decided\n// by the constitution's default voting rules, and an optional sub-DAO\n// tree.\n//\n// Proposal types are registered per DAO: a ProposalKind couples a\n// registry name with a definition factory New(dao ReadonlyCommonDAO,\n// args), and Propose(creator, kind, args) accepts exactly the kinds\n// registered on the DAO. New receives only a readonly view, so it cannot\n// mutate the DAO before the vote; a kind that must mutate state on\n// execution captures its target *CommonDAO from args (populated only by\n// trusted callers) and mutates in its executor. RegisterKind /\n// DeregisterKind / HasKind / KindNames and the WithProposalKind option\n// are the registry primitives; the package ships one concrete kind,\n// ExecutionKind (arbitrary execution), and no governance meta-kinds —\n// managing a DAO's kind set through governance is the consuming realm's\n// job.\n//\n// Proposals snapshot the council as their electorate at creation and\n// are decided the moment the outcome is mathematically settled: with\n// integer math over D = |electorate| - abstains, a supermajority\n// (3*yes \u003e= 2*D) passes, a NO majority (2*no \u003e D) dismisses, and\n// proposals still undecided at their voting deadline are dismissed.\n//\n// A DAO may carry a treasury address and frozen flag; the package only\n// stores them - hosting realms derive the address and move the funds.\n//\n// A *CommonDAO is a mutable handle for the realm that owns it: never\n// accept one from, or return one to, an untrusted realm - readonly views\n// (CommonDAO.Readonly) are the only safe handles to cross a realm\n// boundary. See the package README for details.\n//\n// # Extending commondao in your own realm\n//\n// This package is mostly mechanism: it ships the ExecutionKind concrete kind\n// (with a default voting policy) and the registry primitives, and leaves the\n// rest of governance policy — which kinds a DAO accepts, how it manages them,\n// and any per-kind constraints such as a treasury freeze — to the consuming\n// realm. To add a proposal type of your own:\n//\n//   - Author a ProposalKind: a type with Name() string and\n//     New(dao ReadonlyCommonDAO, args any) (ProposalDefinition, error). Make\n//     the definition Executable (Executor() returns an ExecFunc) if it\n//     mutates state on approval. If its executor moves funds from a DAO other\n//     than the proposal's host, have the HOST realm consume a Funded-style\n//     contract (FundingDAOID() uint64) — minting a DAO sub needs the host's\n//     cur, so this contract is host-consumed, not package-dispatched; define\n//     it in your realm, as the reference realm does.\n//   - Apply your own policy. ExecutionKind runs the closure as-is (no check\n//     beyond a non-nil Fn), so if your realm has treasury constraints (e.g. a\n//     freeze flag) do NOT catalog ExecutionKind directly: author your own\n//     execution kind whose definition wraps the closure with a Validable\n//     check (Validate() error) enforcing those constraints, so arbitrary\n//     execution cannot bypass them. The reference realm does this so a frozen\n//     DAO cannot drain its own treasury through an execution proposal.\n//   - Seed it. The owning realm holds the DAO handle, so no proposal is\n//     needed: pass commondao.New(WithProposalKind(YourKind{}), …) at\n//     construction, or call dao.RegisterKind(YourKind{}) directly.\n//   - Author a typed, CLI-friendly wrapper\n//     CreateYourProposal(cur realm, daoID uint64, …params…): council-gate the\n//     caller, build the args struct, and call Propose. This is the only\n//     public entry, so the args-capture trust boundary holds.\n//   - Optionally add a runtime governance toggle. If the council should\n//     register/deregister kinds by vote (rather than only at construction),\n//     author a manage-kinds-style ProposalKind whose executor calls\n//     RegisterKind/DeregisterKind, and keep that managing kind itself\n//     un-deregisterable so the DAO can always recover.\n//\n// Trust boundary: New receives only a ReadonlyCommonDAO, so a kind — even an\n// externally authored one — cannot mutate the host at Propose time. The\n// mutable *CommonDAO reaches a definition only through args, which your\n// trusted wrapper populates (an external proposer cannot obtain one). On\n// execution the host passes the DAO's terminal, RealmSend-only sub, so a\n// fund-moving executor is bounded to that one DAO address. See the reference\n// realm gno.land/r/nt/commondao/v0 for a full worked example.\npackage commondao\n"},{"name":"execution_kind.gno","body":"package commondao\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\n// executionKindName is the name of the arbitrary-execution kind.\nconst executionKindName = \"execution\"\n\nvar ErrExecutionFuncRequired = errors.New(\"execution proposal requires a non-nil Fn\")\n\n// defaultExecutionVotingPeriod is the voting period of execution proposals.\nconst defaultExecutionVotingPeriod = 7 * 24 * time.Hour\n\n// ExecutionArgs are the args for the execution kind (ExecutionKind): a\n// title, a body, and the ExecFunc to run on approval. The closure must be\n// authored in a persistent realm so it survives Propose→Execute; a\n// closure created by a `maketx run` script does not persist and cannot be\n// executed later.\ntype ExecutionArgs struct {\n\tTitle string\n\tBody  string\n\tFn    ExecFunc\n}\n\n// ExecutionKind is the package's one concrete proposal kind: a stateless,\n// reusable arbitrary-execution kind that runs an ExecFunc supplied by the\n// proposer on approval. It is /p/-typed so any realm can register it with\n// WithProposalKind(ExecutionKind{}) or RegisterKind without defining its\n// own execution kind.\n//\n// The executor moves value only through the DAO-scoped sub the host\n// passes (see ExecFunc): the host mints and passes that sub, so the\n// executor receives whichever DAO's sub the host decides (its own DAO's by\n// default). The closure is frozen at Propose (vote-integrity: the exact\n// code is fixed before the vote).\n//\n// This kind applies NO policy check to the closure beyond a non-nil Fn: it\n// runs the arbitrary code as-is. A realm that has treasury constraints (e.g.\n// a freeze flag) should NOT catalog this kind directly; instead it should\n// author its own execution kind whose definition wraps the closure with a\n// Validable check that enforces those constraints (blocking execution while\n// frozen, etc.), so arbitrary execution cannot bypass them. The reference\n// realm gno.land/r/nt/commondao/v0 does exactly this.\ntype ExecutionKind struct{}\n\n// Name returns the execution kind's registry name.\nfunc (ExecutionKind) Name() string { return executionKindName }\n\n// New validates ExecutionArgs and builds an execution definition. It is a\n// pure factory: it receives only a readonly view and captures no mutable\n// handle.\nfunc (ExecutionKind) New(_ ReadonlyCommonDAO, args any) (ProposalDefinition, error) {\n\ta, ok := args.(ExecutionArgs)\n\tif !ok || a.Fn == nil {\n\t\treturn nil, ErrExecutionFuncRequired\n\t}\n\treturn executionDef{title: a.Title, body: a.Body, fn: a.Fn}, nil\n}\n\n// executionDef is the definition produced by ExecutionKind.\ntype executionDef struct {\n\ttitle string\n\tbody  string\n\tfn    ExecFunc\n}\n\nfunc (d executionDef) Title() string             { return d.title }\nfunc (d executionDef) Body() string              { return d.body }\nfunc (executionDef) VotingPeriod() time.Duration { return defaultExecutionVotingPeriod }\nfunc (executionDef) Threshold() Threshold        { return ThresholdSupermajority }\nfunc (d executionDef) Executor() ExecFunc        { return d.fn }\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/commondao/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"},{"name":"proposal.gno","body":"package commondao\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/nt/addrset/v0\"\n)\n\nconst (\n\tStatusActive    ProposalStatus = \"active\"\n\tStatusPassed    ProposalStatus = \"passed\"\n\tStatusDismissed ProposalStatus = \"dismissed\"\n\tStatusExecuted  ProposalStatus = \"executed\"\n\tStatusFailed    ProposalStatus = \"failed\"\n\tStatusWithdrawn ProposalStatus = \"withdrawn\"\n)\n\n// Vote choices, fixed by the Common DAO Spec's default voting rules.\nconst (\n\tChoiceYes     VoteChoice = \"YES\"\n\tChoiceNo      VoteChoice = \"NO\"\n\tChoiceAbstain VoteChoice = \"ABSTAIN\"\n)\n\n// Thresholds for the constitution's default Council voting rules.\nconst (\n\t// ThresholdSupermajority passes with \"two thirds or more\" of the\n\t// tally denominator. The default for Council decisions.\n\tThresholdSupermajority Threshold = iota\n\n\t// ThresholdSimpleMajority passes with \"more than half\" of the\n\t// tally denominator. The Constitution assigns it to specific\n\t// decisions, e.g. sub-DAO creation.\n\tThresholdSimpleMajority\n)\n\n// Outcomes of tallying a proposal under the default Council rules.\nconst (\n\tOutcomePending Outcome = iota\n\tOutcomePassed\n\tOutcomeDismissed\n)\n\nvar (\n\tErrInvalidCreatorAddress      = errors.New(\"invalid proposal creator address\")\n\tErrInvalidVoterAddress        = errors.New(\"invalid voter address\")\n\tErrProposalDefinitionRequired = errors.New(\"proposal definition is required\")\n\tErrStatusIsNotActive          = errors.New(\"proposal status is not active\")\n)\n\ntype (\n\t// ProposalStatus defines a type for different proposal states.\n\tProposalStatus string\n\n\t// VoteChoice defines a type for proposal vote choices.\n\tVoteChoice string\n\n\t// Threshold defines a type for the default tally thresholds.\n\tThreshold int\n\n\t// Outcome defines a type for default tally outcomes.\n\tOutcome int\n\n\t// ExecFunc defines a type for functions that execute proposals.\n\t//\n\t// The leading int makes ExecFunc non-crossing: the host calls it\n\t// directly (no cross), so the executor holds no realm cur of its own —\n\t// only the realm argument, a DAO-scoped sub-identity the host mints and\n\t// passes. Fund-moving executors send through that sub (e.g. banker\n\t// RealmSend), which is terminal and bounded to one DAO address;\n\t// executors that move no funds ignore it. The int is unused.\n\t//\n\t// Authority note: the sub is a least-authority DEFAULT, not a sandbox.\n\t// An executor is trusted realm code; because the sub is the executor's\n\t// only current realm value, it could regain the host realm's primary\n\t// authority via an explicit cross(sub) into a crossing function. That is\n\t// a visible, auditable call the reference realm's executors never make,\n\t// so their blast radius is one treasury — but a realm that runs\n\t// untrusted or user-registered executors gets no such guarantee. See ADR\n\t// pr6012_commondao_exec_scope.\n\t//\n\t// The sharper hazard for such a realm is not cross(sub) but the banker:\n\t// an executor can mint banker.NewBanker(BankerTypeRealmSend, sub) and\n\t// simply RETAIN it. Authorization happens at construction only, and the\n\t// banker holds no realm reference, so it persists across transactions\n\t// even though the sub itself cannot — a permanent, unrevocable\n\t// capability over that DAO's address, spendable later with no proposal.\n\t// It also bypasses any check the host performs before spending (a\n\t// frozen flag, a pause switch), because it reaches the bank keeper\n\t// without re-entering host code. Passing the sub to an executor whose\n\t// code the DAO has not vetted is therefore an irrevocable grant of that\n\t// DAO's treasury, not a scoped loan of it.\n\tExecFunc func(int, realm) error\n\n\t// Proposal defines a DAO proposal.\n\tProposal struct {\n\t\tid             uint64\n\t\tstatus         ProposalStatus\n\t\tdefinition     ProposalDefinition\n\t\tcreator        address\n\t\trecord         *VotingRecord\n\t\telectorate     *addrset.Set // council snapshot taken at Propose\n\t\tstatusReason   string\n\t\tvotingDeadline time.Time\n\t\tcreatedAt      time.Time\n\t}\n\n\t// ProposalDefinition defines an interface for custom proposal definitions.\n\t// These definitions define proposal content and behavior, essentially\n\t// allowing the definition of different proposal types.\n\tProposalDefinition interface {\n\t\t// Title returns the proposal title.\n\t\tTitle() string\n\n\t\t// Body returns proposal's body.\n\t\t// It usually contains description or values that are specific to the proposal,\n\t\t// like a description of the proposal's motivation or the list of values that\n\t\t// would be applied when the proposal is approved.\n\t\tBody() string\n\n\t\t// VotingPeriod returns the period where votes are allowed after proposal creation.\n\t\t// It is used to calculate the voting deadline from the proposal's creation date.\n\t\tVotingPeriod() time.Duration\n\n\t\t// Threshold returns the tally threshold for passing the proposal.\n\t\t// Proposals are decided by the constitution's default Council voting\n\t\t// rules: re-evaluated after every recorded vote, they can pass or be\n\t\t// dismissed before their voting deadline.\n\t\t//\n\t\t// Threshold is read on every Vote (for early passage) AND again in\n\t\t// the post-deadline re-tally inside Execute. Return a CONSTANT value:\n\t\t// a threshold that loosens over a proposal's lifetime can let the\n\t\t// deadline re-tally pass with fewer YES votes than voters faced when\n\t\t// they cast under the stricter earlier value. A changing threshold is\n\t\t// honored, but the definition author owns that consequence.\n\t\tThreshold() Threshold\n\t}\n\n\t// ProposalKind defines an interface for proposal kinds: named factories\n\t// for proposal definitions, registered per DAO. A kind is both the\n\t// registry key (Name) and the factory (New) for one proposal type, and\n\t// a DAO accepts proposals of exactly the kinds registered on it\n\t// (CommonDAO.RegisterKind).\n\tProposalKind interface {\n\t\t// Name returns the kind name used as registry key, e.g. \"treasury-spend\".\n\t\tName() string\n\n\t\t// New validates args and builds the proposal definition. Propose\n\t\t// passes a ReadonlyCommonDAO view of the host DAO, so New is a\n\t\t// pure factory that cannot mutate the host or its tree before the\n\t\t// vote; proposal targets and parameters come via args. A kind that\n\t\t// must mutate state on execution receives the target *CommonDAO\n\t\t// through args (which only trusted callers can populate), captures\n\t\t// it, and mutates in its Executor. The returned definition's\n\t\t// instance data is frozen at Propose like any proposal definition.\n\t\tNew(dao ReadonlyCommonDAO, args any) (ProposalDefinition, error)\n\t}\n\n\t// CapExempt defines an interface for proposal definitions that are not\n\t// counted against the DAO's active proposals cap. Exempt definitions are\n\t// instead bounded to one active proposal per creator, so that proposals\n\t// which remove members (and therefore must never be blockable by a full\n\t// cap) stay bounded.\n\tCapExempt interface {\n\t\t// CapExempt marks the definition as exempt.\n\t\tCapExempt()\n\t}\n\n\t// Validable defines an interface for proposal definitions that require state validation.\n\t// Validation is done before execution and normally also during proposal rendering.\n\tValidable interface {\n\t\t// Validate validates that the proposal is valid for the current state.\n\t\tValidate() error\n\t}\n\n\t// Executable defines an interface for proposal definitions that modify state on approval.\n\t// Once proposals are executed they are archived and considered finished.\n\tExecutable interface {\n\t\t// Executor returns a function to execute the proposal.\n\t\tExecutor() ExecFunc\n\t}\n)\n\n// newProposal creates a new DAO proposal.\n//\n// The proposal is created with an empty electorate; Propose populates it\n// with the council snapshot.\nfunc newProposal(id uint64, creator address, d ProposalDefinition) (*Proposal, error) {\n\tif !creator.IsValid() {\n\t\treturn nil, ErrInvalidCreatorAddress\n\t}\n\n\tnow := time.Now()\n\treturn \u0026Proposal{\n\t\tid:             id,\n\t\tstatus:         StatusActive,\n\t\tdefinition:     d,\n\t\tcreator:        creator,\n\t\trecord:         \u0026VotingRecord{},\n\t\telectorate:     \u0026addrset.Set{},\n\t\tvotingDeadline: now.Add(d.VotingPeriod()),\n\t\tcreatedAt:      now,\n\t}, nil\n}\n\n// ID returns the unique proposal identifier.\nfunc (p Proposal) ID() uint64 {\n\treturn p.id\n}\n\n// Definition returns the proposal definition.\n// Proposal definitions define proposal content and behavior.\nfunc (p Proposal) Definition() ProposalDefinition {\n\treturn p.definition\n}\n\n// Status returns the current proposal status.\nfunc (p Proposal) Status() ProposalStatus {\n\treturn p.status\n}\n\n// Creator returns the address of the account that created the proposal.\nfunc (p Proposal) Creator() address {\n\treturn p.creator\n}\n\n// CreatedAt returns the time that proposal was created.\nfunc (p Proposal) CreatedAt() time.Time {\n\treturn p.createdAt\n}\n\n// VotingRecord returns a read only record with the votes submitted for\n// the proposal. Votes are recorded through CommonDAO.Vote only.\nfunc (p Proposal) VotingRecord() ReadonlyVotingRecord {\n\treturn p.record.Readonly()\n}\n\n// Electorate returns the proposal's electorate: a read only view of the\n// council snapshot taken when the proposal was created. Members added to\n// the council afterwards vote on the next proposal; members removed or\n// resigned afterwards remain in the electorate (their silence counts\n// against passage).\nfunc (p Proposal) Electorate() *addrset.ReadonlySet {\n\treturn p.electorate.Readonly()\n}\n\n// StatusReason returns an optional reason that led to the current proposal status.\n// Reason is mostly useful when a proposal fails.\nfunc (p Proposal) StatusReason() string {\n\treturn p.statusReason\n}\n\n// VotingDeadline returns the deadline after which no more votes should be allowed.\nfunc (p Proposal) VotingDeadline() time.Time {\n\treturn p.votingDeadline\n}\n\n// HasVotingDeadlinePassed checks if the voting deadline has been met.\nfunc (p Proposal) HasVotingDeadlinePassed() bool {\n\treturn !time.Now().Before(p.VotingDeadline())\n}\n\n// Validate validates that a proposal is valid for the current state.\n// Validation is done when the proposal can still be executed (status is\n// active or passed) and when the definition supports validation.\nfunc (p Proposal) Validate() error {\n\tif p.status != StatusActive \u0026\u0026 p.status != StatusPassed {\n\t\treturn nil\n\t}\n\n\tif v, ok := p.definition.(Validable); ok {\n\t\treturn v.Validate()\n\t}\n\treturn nil\n}\n\n// ExpectedOutcome returns the outcome the proposal would have if it were\n// decided with the votes submitted so far. Useful for rendering.\nfunc (p Proposal) ExpectedOutcome() Outcome {\n\treturn TallyDefault(p.record.Readonly(), p.Electorate(), p.definition.Threshold())\n}\n"},{"name":"proposal_storage.gno","body":"package commondao\n\nimport (\n\t\"gno.land/p/nt/bptree/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// newProposalStorage creates a new proposal storage.\nfunc newProposalStorage() *proposalStorage {\n\treturn \u0026proposalStorage{bptree.NewBPTree32()}\n}\n\n// proposalStorage stores proposals indexed by ID.\ntype proposalStorage struct {\n\tstorage *bptree.BPTree // string(proposal ID) -\u003e *Proposal\n}\n\n// Get returns a proposal or nil when proposal doesn't exist.\nfunc (s proposalStorage) Get(id uint64) *Proposal {\n\tif v := s.storage.Get(makeProposalKey(id)); v != nil {\n\t\treturn v.(*Proposal)\n\t}\n\treturn nil\n}\n\n// Add adds a proposal to the storage.\nfunc (s *proposalStorage) Add(p *Proposal) {\n\tif p == nil {\n\t\treturn\n\t}\n\n\ts.storage.Set(makeProposalKey(p.ID()), p)\n}\n\n// Remove removes a proposal from the storage.\nfunc (s *proposalStorage) Remove(id uint64) {\n\ts.storage.Remove(makeProposalKey(id))\n}\n\n// Size returns the number of proposals that the storage contains.\nfunc (s proposalStorage) Size() int {\n\treturn s.storage.Size()\n}\n\n// Iterate iterates proposals.\nfunc (s proposalStorage) Iterate(offset, count int, reverse bool, fn func(*Proposal) bool) bool {\n\tcb := func(_ string, v any) bool { return fn(v.(*Proposal)) }\n\n\tif reverse {\n\t\treturn s.storage.ReverseIterateByOffset(offset, count, cb)\n\t}\n\treturn s.storage.IterateByOffset(offset, count, cb)\n}\n\nfunc makeProposalKey(id uint64) string {\n\treturn seqid.ID(id).String()\n}\n"},{"name":"readonly.gno","body":"package commondao\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/nt/addrset/v0\"\n)\n\n// ReadonlyCommonDAO is a read only view of a CommonDAO. It exposes only\n// read side methods, holds the *CommonDAO in an unexported field, and every\n// reachable value is itself readonly or a copy — so cross-realm holders\n// cannot mutate the DAO through it. Views are live handles, not snapshots.\n//\n// This is the only safe handle to a DAO across a realm boundary: hosting\n// realms must never return the *CommonDAO itself.\ntype ReadonlyCommonDAO struct {\n\tdao *CommonDAO\n}\n\n// Readonly returns a read only view of the DAO.\nfunc (dao *CommonDAO) Readonly() ReadonlyCommonDAO {\n\treturn ReadonlyCommonDAO{dao}\n}\n\n// ID returns DAO's unique identifier.\nfunc (r ReadonlyCommonDAO) ID() uint64 {\n\treturn r.dao.id\n}\n\n// Name returns DAO's name.\nfunc (r ReadonlyCommonDAO) Name() string {\n\treturn r.dao.name\n}\n\n// Purpose returns DAO's purpose (part of the Charter).\nfunc (r ReadonlyCommonDAO) Purpose() string {\n\treturn r.dao.purpose\n}\n\n// Description returns DAO's description.\nfunc (r ReadonlyCommonDAO) Description() string {\n\treturn r.dao.description\n}\n\n// Address returns the DAO's treasury address, or empty when unset.\nfunc (r ReadonlyCommonDAO) Address() address {\n\treturn r.dao.addr\n}\n\n// IsDeleted returns true when DAO has been soft deleted.\nfunc (r ReadonlyCommonDAO) IsDeleted() bool {\n\treturn r.dao.deleted\n}\n\n// IsTreasuryFrozen checks if the DAO's treasury is frozen.\nfunc (r ReadonlyCommonDAO) IsTreasuryFrozen() bool {\n\treturn r.dao.treasuryFrozen\n}\n\n// Council returns a read only view of the DAO council.\nfunc (r ReadonlyCommonDAO) Council() *addrset.ReadonlySet {\n\treturn r.dao.council.Readonly()\n}\n\n// HasKind checks if a proposal kind is registered on the DAO.\nfunc (r ReadonlyCommonDAO) HasKind(name string) bool {\n\treturn r.dao.HasKind(name)\n}\n\n// KindNames returns the names of the registered proposal kinds, sorted.\nfunc (r ReadonlyCommonDAO) KindNames() []string {\n\treturn r.dao.KindNames()\n}\n\n// Parent returns a read only view of the parent DAO when there is one.\nfunc (r ReadonlyCommonDAO) Parent() (_ ReadonlyCommonDAO, found bool) {\n\tif r.dao.parent == nil {\n\t\treturn ReadonlyCommonDAO{}, false\n\t}\n\treturn r.dao.parent.Readonly(), true\n}\n\n// ChildrenCount returns the number of direct children DAOs.\nfunc (r ReadonlyCommonDAO) ChildrenCount() int {\n\treturn r.dao.children.Len()\n}\n\n// IterateChildren iterates the direct children DAOs.\n// The callback can return true to stop iteration.\nfunc (r ReadonlyCommonDAO) IterateChildren(fn func(ReadonlyCommonDAO) bool) (stopped bool) {\n\tr.dao.children.ForEach(func(_ int, v any) bool {\n\t\tstopped = fn(v.(*CommonDAO).Readonly())\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// GetProposal returns a read only view of a proposal when it exists.\nfunc (r ReadonlyCommonDAO) GetProposal(proposalID uint64) (_ ReadonlyProposal, found bool) {\n\tp := r.dao.GetProposal(proposalID)\n\tif p == nil {\n\t\treturn ReadonlyProposal{}, false\n\t}\n\treturn p.Readonly(), true\n}\n\n// ActiveProposalsSize returns the number of active proposals.\nfunc (r ReadonlyCommonDAO) ActiveProposalsSize() int {\n\treturn r.dao.activeProposals.Size()\n}\n\n// FinishedProposalsSize returns the number of finished proposals.\nfunc (r ReadonlyCommonDAO) FinishedProposalsSize() int {\n\treturn r.dao.finishedProposals.Size()\n}\n\n// IterateActiveProposals iterates read only views of the active proposals.\n// The callback can return true to stop iteration.\nfunc (r ReadonlyCommonDAO) IterateActiveProposals(offset, count int, reverse bool, fn func(ReadonlyProposal) bool) bool {\n\treturn r.dao.activeProposals.Iterate(offset, count, reverse, func(p *Proposal) bool {\n\t\treturn fn(p.Readonly())\n\t})\n}\n\n// IterateFinishedProposals iterates read only views of the finished proposals.\n// The callback can return true to stop iteration.\nfunc (r ReadonlyCommonDAO) IterateFinishedProposals(offset, count int, reverse bool, fn func(ReadonlyProposal) bool) bool {\n\treturn r.dao.finishedProposals.Iterate(offset, count, reverse, func(p *Proposal) bool {\n\t\treturn fn(p.Readonly())\n\t})\n}\n\n// ReadonlyProposal is a read only view of a Proposal. Proposal content is\n// exposed flattened (Title, Body): the view never exposes the underlying\n// ProposalDefinition, whose Executor would otherwise be callable by any\n// holder with the hosting realm's authority.\ntype ReadonlyProposal struct {\n\tp *Proposal\n}\n\n// Readonly returns a read only view of the proposal.\nfunc (p *Proposal) Readonly() ReadonlyProposal {\n\treturn ReadonlyProposal{p}\n}\n\n// ID returns the unique proposal identifier.\nfunc (r ReadonlyProposal) ID() uint64 {\n\treturn r.p.id\n}\n\n// Status returns the current proposal status.\nfunc (r ReadonlyProposal) Status() ProposalStatus {\n\treturn r.p.status\n}\n\n// StatusReason returns an optional reason that led to the current proposal status.\nfunc (r ReadonlyProposal) StatusReason() string {\n\treturn r.p.statusReason\n}\n\n// Creator returns the address of the account that created the proposal.\nfunc (r ReadonlyProposal) Creator() address {\n\treturn r.p.creator\n}\n\n// CreatedAt returns the time that proposal was created.\nfunc (r ReadonlyProposal) CreatedAt() time.Time {\n\treturn r.p.createdAt\n}\n\n// VotingDeadline returns the deadline after which no more votes are allowed.\nfunc (r ReadonlyProposal) VotingDeadline() time.Time {\n\treturn r.p.votingDeadline\n}\n\n// Title returns the proposal definition's title.\nfunc (r ReadonlyProposal) Title() string {\n\treturn r.p.definition.Title()\n}\n\n// Body returns the proposal definition's body.\nfunc (r ReadonlyProposal) Body() string {\n\treturn r.p.definition.Body()\n}\n\n// VotingRecord returns a read only record with the submitted votes.\nfunc (r ReadonlyProposal) VotingRecord() ReadonlyVotingRecord {\n\treturn r.p.record.Readonly()\n}\n\n// Electorate returns a read only view of the proposal's electorate.\nfunc (r ReadonlyProposal) Electorate() *addrset.ReadonlySet {\n\treturn r.p.Electorate()\n}\n"},{"name":"record.gno","body":"package commondao\n\nimport (\n\t\"gno.land/p/nt/addrset/v0\"\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\ntype (\n\t// VoteIterFn defines a callback to iterate votes.\n\tVoteIterFn func(Vote) (stop bool)\n\n\t// VotesCountIterFn defines a callback to iterate voted choices.\n\tVotesCountIterFn func(_ VoteChoice, voteCount int) (stop bool)\n\n\t// Vote defines a single vote. Its fields are unexported so instances\n\t// cannot be forged or reshaped outside the package: votes enter a\n\t// record only through CommonDAO.Vote's gates.\n\tVote struct {\n\t\taddr   address\n\t\tchoice VoteChoice\n\t\treason string\n\t}\n)\n\n// NewVote creates a vote, validating the address and choice. It exists\n// so external code can build records to independently re-verify a tally\n// with TallyDefault; votes reach a DAO's own records only through\n// CommonDAO.Vote.\nfunc NewVote(addr address, choice VoteChoice, reason string) (Vote, error) {\n\tif !addr.IsValid() {\n\t\treturn Vote{}, ErrInvalidVoterAddress\n\t}\n\n\tif choice != ChoiceYes \u0026\u0026 choice != ChoiceNo \u0026\u0026 choice != ChoiceAbstain {\n\t\treturn Vote{}, ErrInvalidVoteChoice\n\t}\n\n\treturn Vote{addr: addr, choice: choice, reason: reason}, nil\n}\n\n// Address returns the address of the account that submitted the vote.\nfunc (v Vote) Address() address {\n\treturn v.addr\n}\n\n// Choice returns the voted choice.\nfunc (v Vote) Choice() VoteChoice {\n\treturn v.choice\n}\n\n// Reason returns the optional reason for the vote.\nfunc (v Vote) Reason() string {\n\treturn v.reason\n}\n\n// ReadonlyVotingRecord defines a read only voting record. The copy\n// captures the live record's tree roots by value, so a held value can go\n// stale in surprising ways: fetch it, read it, and re-fetch rather than\n// holding it across votes.\ntype ReadonlyVotingRecord struct {\n\tvotes bptree.BPTree // string(address) -\u003e Vote\n\tcount bptree.BPTree // string(choice) -\u003e int\n}\n\n// Size returns the total number of votes that record contains.\nfunc (r ReadonlyVotingRecord) Size() int {\n\treturn r.votes.Size()\n}\n\n// Iterate iterates voting record votes.\nfunc (r ReadonlyVotingRecord) Iterate(offset, count int, reverse bool, fn VoteIterFn) bool {\n\tcb := func(_ string, v any) bool { return fn(v.(Vote)) }\n\tif reverse {\n\t\treturn r.votes.ReverseIterateByOffset(offset, count, cb)\n\t}\n\treturn r.votes.IterateByOffset(offset, count, cb)\n}\n\n// IterateVotesCount iterates voted choices with the amount of votes submitted for each.\nfunc (r ReadonlyVotingRecord) IterateVotesCount(fn VotesCountIterFn) bool {\n\treturn r.count.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\treturn fn(VoteChoice(k), v.(int))\n\t})\n}\n\n// VoteCount returns the number of votes for a single voting choice.\nfunc (r ReadonlyVotingRecord) VoteCount(c VoteChoice) int {\n\tif v := r.count.Get(string(c)); v != nil {\n\t\treturn v.(int)\n\t}\n\treturn 0\n}\n\n// HasVoted checks if an account already voted.\nfunc (r ReadonlyVotingRecord) HasVoted(user address) bool {\n\treturn r.votes.Has(user.String())\n}\n\n// GetVote returns a vote.\nfunc (r ReadonlyVotingRecord) GetVote(user address) (_ Vote, found bool) {\n\tif v := r.votes.Get(user.String()); v != nil {\n\t\treturn v.(Vote), true\n\t}\n\treturn Vote{}, false\n}\n\n// VotingRecord stores accounts that voted and vote choices.\ntype VotingRecord struct {\n\tReadonlyVotingRecord\n}\n\n// Readonly returns a read only voting record.\nfunc (r VotingRecord) Readonly() ReadonlyVotingRecord {\n\treturn r.ReadonlyVotingRecord\n}\n\n// AddVote adds a vote to the voting record.\n// If a vote for the same user already exists is overwritten.\nfunc (r *VotingRecord) AddVote(vote Vote) (updated bool) {\n\t// Get previous member vote if it exists\n\tv := r.votes.Get(vote.addr.String())\n\n\t// When a previous vote exists update counter for the previous choice\n\tupdated = r.votes.Set(vote.addr.String(), vote)\n\tif updated {\n\t\tprev := v.(Vote)\n\t\tr.count.Set(string(prev.choice), r.VoteCount(prev.choice)-1)\n\t}\n\n\tr.count.Set(string(vote.choice), r.VoteCount(vote.choice)+1)\n\treturn\n}\n\n// TallyDefault applies the constitution's default Council voting rules\n// over a proposal's electorate.\n//\n// Only votes cast by electorate members are counted. The tally denominator\n// D is the electorate size minus the number of ABSTAIN votes: abstaining\n// shrinks the denominator (deference), while not voting counts against\n// passage (silence is opposition). With integer math:\n//\n//\tD = |electorate| - abstains\n//\tsupermajority:   passed    ⇔ D \u003e 0 \u0026\u0026 3*yes \u003e= 2*D\n//\tsimple majority: passed    ⇔ D \u003e 0 \u0026\u0026 2*yes \u003e D\n//\tboth:            dismissed ⇔ 2*no \u003e D\n//\n// Passing is checked before dismissal; within one electorate both can never\n// hold at once (yes+no \u003c= D makes each pair contradictory). When D is zero\n// or negative (an empty electorate, or every member abstained) the outcome\n// stays pending: nothing can pass with zero YES votes.\nfunc TallyDefault(r ReadonlyVotingRecord, electorate *addrset.ReadonlySet, t Threshold) Outcome {\n\tvar yes, no, abstain int\n\tr.Iterate(0, r.Size(), false, func(v Vote) bool {\n\t\tif !electorate.Has(v.addr) {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch v.choice {\n\t\tcase ChoiceYes:\n\t\t\tyes++\n\t\tcase ChoiceNo:\n\t\t\tno++\n\t\tcase ChoiceAbstain:\n\t\t\tabstain++\n\t\t}\n\t\treturn false\n\t})\n\n\td := electorate.Size() - abstain\n\tif d \u003c= 0 {\n\t\treturn OutcomePending\n\t}\n\n\tswitch t {\n\tcase ThresholdSimpleMajority:\n\t\tif 2*yes \u003e d {\n\t\t\treturn OutcomePassed\n\t\t}\n\tdefault:\n\t\tif 3*yes \u003e= 2*d {\n\t\t\treturn OutcomePassed\n\t\t}\n\t}\n\n\tif 2*no \u003e d {\n\t\treturn OutcomeDismissed\n\t}\n\treturn OutcomePending\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"p_crossrealm","path":"gno.land/p/demo/tests/p_crossrealm","files":[{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests/p_crossrealm\"\ngno = \"0.9\"\n"},{"name":"p_crossrealm.gno","body":"package p_crossrealm\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n)\n\ntype Stringer interface {\n\tString() string\n}\n\ntype Container struct {\n\tA int\n\tB Stringer\n}\n\nfunc (c *Container) Touch() *Container {\n\tc.A += 1\n\treturn c\n}\n\nfunc (c *Container) Print() {\n\tprintln(\"A:\", c.A)\n\tif c.B == nil {\n\t\tprintln(\"B: undefined\")\n\t} else {\n\t\tprintln(\"B:\", c.B.String())\n\t}\n}\n\nfunc CurrentRealm() runtime.Realm {\n\treturn unsafe.CurrentRealm()\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"crossrealm","path":"gno.land/r/tests/vm/crossrealm","files":[{"name":"crossrealm.gno","body":"package crossrealm\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/p/demo/tests/p_crossrealm\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype LocalStruct struct {\n\tA int\n}\n\nfunc (ls *LocalStruct) String() string {\n\treturn ufmt.Sprintf(\"LocalStruct{%d}\", ls.A)\n}\n\n// local is saved locally in this realm\nvar local *LocalStruct\n\nfunc init() {\n\tlocal = \u0026LocalStruct{A: 123}\n}\n\n// Make1 returns a local object wrapped by a p struct\nfunc Make1() *p_crossrealm.Container {\n\treturn \u0026p_crossrealm.Container{\n\t\tA: 1,\n\t\tB: local,\n\t}\n}\n\ntype Fooer interface {\n\tFoo(realm)\n\tBar()\n}\n\nvar fooer Fooer\n\nfunc SetFooer(cur realm, f Fooer) Fooer {\n\tfooer = f\n\treturn fooer\n}\n\nfunc GetFooer() Fooer {\n\treturn fooer\n}\n\nfunc CallFooerFooCur(cur realm) {\n\tfooer.Foo(cur)\n}\n\nfunc CallFooerFooCross(cur realm) {\n\tfooer.Foo(cross(cur))\n}\n\nfunc CallFooerBar() {\n\tfooer.Bar()\n}\n\nfunc CallFooerBarCrossing(cur realm) {\n\tfooer.Bar()\n}\n\ntype FooerGetter func() Fooer\n\nvar fooerGetter FooerGetter\n\nfunc SetFooerGetter(cur realm, fg FooerGetter) FooerGetter {\n\tfooerGetter = fg\n\treturn fg\n}\n\nfunc GetFooerGetter() FooerGetter {\n\treturn fooerGetter\n}\n\nfunc CallFooerGetterBar() {\n\tfooerGetter().Bar()\n}\n\nfunc CallFooerGetterBarCrossing(cur realm) {\n\tfooerGetter().Bar()\n}\n\nfunc CallFooerGetterFooCur(cur realm) {\n\tfooerGetter().Foo(cur)\n}\n\nfunc CallFooerGetterFooCross(cur realm) {\n\tfooerGetter().Foo(cross(cur))\n}\n\n// This is a top function that does switch realms.\nfunc ExecCrossing(cur realm, cb func() string) string {\n\treturn cb()\n}\n\n// This is a top function that doesn't switch realms.\nfunc Exec(cb func() string) string {\n\treturn cb()\n}\n\n// ------------------------------------\n// SECURITY XXX: Closure and Closure2 below are exported package-level\n// function-typed vars that any foreign realm can set via SetClosure /\n// SetClosure2 and trigger via ExecuteClosure / ExecuteClosureCross.\n// Closure2's signature is `func(realm)`, which means whoever sets it\n// receives a realm value at invocation time — capability-bearing data\n// stored in an exported package var, then handed back to caller-supplied\n// code. THIS IS DELIBERATE VM-PARITY-TEST INFRASTRUCTURE — these vars\n// exist to exercise the VM's handling of stored closures and cross-\n// realm callbacks. DO NOT COPY THIS PATTERN IN PRODUCTION CODE. Real\n// /r/ realms must never expose `func(realm)` (or any function-typed\n// value capable of receiving cur) as an exported package var.\nvar Closure func()\n\nfunc SetClosure(cur realm, f func()) {\n\tClosure = f\n}\n\nfunc ExecuteClosure(cur realm) {\n\tClosure()\n}\n\nvar Closure2 func(realm)\n\nfunc SetClosure2(cur realm, f func(realm)) {\n\tClosure2 = f\n}\nfunc ExecuteClosureCross(cur realm) {\n\tClosure2(cross(cur))\n}\n\n// Closure3 mirrors Closure but for non-crossing-with-rlm closures —\n// `func(_ int, rlm realm)`. Whoever sets Closure3 receives cur as a\n// plain value (no realm boundary at invocation time), then can cross\n// internally via cross(rlm).\nvar Closure3 func(_ int, rlm realm)\n\nfunc SetClosure3(cur realm, f func(_ int, rlm realm)) {\n\tClosure3 = f\n}\n\nfunc ExecuteClosure3(cur realm) {\n\tClosure3(0, cur)\n}\n\n// Closure -\u003e FooUpdate\nfunc PrintRealms(cur realm) {\n\tufmt.Printf(\"current realm: %s\\n\", unsafe.CurrentRealm())\n\tufmt.Printf(\"previous realm: %s\\n\", unsafe.PreviousRealm())\n}\n\n// -------------------------------------------------\nvar Object any\n\nfunc SetObject(cur realm, x any) {\n\tObject = x\n}\n\nfunc GetObject() any {\n\treturn Object\n}\n\nfunc EntryPoint() (noCros *ownable.Ownable) {\n\tprintln(\"crossrealm  EntryPoint: \" + unsafe.PreviousRealm().PkgPath())\n\tprintln(\"crossrealm  EntryPoint: \" + unsafe.PreviousRealm().Address())\n\tprintln()\n\treturn PrevRealmNoCrossing()\n}\n\n// EntryPointWithCrossing is a non-crossing helper that forwards into\n// the (cur realm)-crossing PrevRealmCrossing. Callers pass their own\n// live cur; `cross(rlm)` does the actual cross.\nfunc EntryPointWithCrossing(_ int, rlm realm) (withCros *ownable.Ownable) {\n\treturn PrevRealmCrossing(cross(rlm))\n}\n\nfunc PrevRealmNoCrossing() *ownable.Ownable {\n\tprintln(\"crossrealm PreviousRealm no crossing: \" + unsafe.PreviousRealm().PkgPath())\n\tprintln(\"crossrealm PreviousRealm no crossing: \" + unsafe.PreviousRealm().Address())\n\treturn ownable.NewWithAddress(unsafe.CurrentRealm().Address())\n}\n\nfunc PrevRealmCrossing(cur realm) *ownable.Ownable {\n\tprintln(\"crossrealm PreviousRealm with crossing: \" + unsafe.PreviousRealm().PkgPath())\n\tprintln(\"crossrealm PreviousRealm with crossing: \" + unsafe.PreviousRealm().Address())\n\treturn ownable.NewWithAddress(cur.Address())\n}\n\nfunc CurRealmNoCrossing() runtime.Realm {\n\treturn unsafe.CurrentRealm()\n}\nfunc CurRealmCrossing(cur realm) runtime.Realm {\n\treturn unsafe.CurrentRealm()\n}\n\n// call the package that returns current realm\nfunc PkgCurRealmNoCrossing() runtime.Realm {\n\treturn p_crossrealm.CurrentRealm()\n}\n\n// call the package that returns current realm\nfunc PkgCurRealmCrossing(cur realm) runtime.Realm {\n\treturn unsafe.CurrentRealm()\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/crossrealm\"\ngno = \"0.9\"\n"},{"name":"switchrealm.gno","body":"package crossrealm\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"crossrealm_b","path":"gno.land/r/tests/vm/crossrealm_b","files":[{"name":"crossrealm.gno","body":"package crossrealm_b\n\nimport (\n\t\"chain/banker\"\n\t\"chain/runtime/unsafe\"\n\n\t\"gno.land/r/tests/vm/crossrealm\"\n)\n\ntype fooer struct {\n\ts string\n}\n\nfunc (f *fooer) SetS(newVal string) {\n\tf.s = newVal\n}\n\nfunc (f *fooer) Foo(cur realm) {\n\tprintln(\"hello \" + f.s + \" cur=\" + unsafe.CurrentRealm().PkgPath() + \" prev=\" + unsafe.PreviousRealm().PkgPath())\n}\n\nfunc (f *fooer) Bar() {\n\tprintln(\"hello \" + f.s + \" cur=\" + unsafe.CurrentRealm().PkgPath() + \" prev=\" + unsafe.PreviousRealm().PkgPath())\n}\n\nvar (\n\tFooer              = \u0026fooer{s: \"A\"}\n\tFooerGetter        = func() crossrealm.Fooer { return Fooer }\n\tFooerGetterBuilder = func() crossrealm.FooerGetter { return func() crossrealm.Fooer { return Fooer } }\n)\n\nvar Closure func()\n\nfunc SetClosure(cur realm, f func()) {\n\tClosure = f\n}\n\nvar Object any\n\nfunc SetObject(cur realm, x any) {\n\tObject = x\n}\n\nfunc GetObject() any {\n\treturn Object\n}\n\nfunc IncrementObject(cur realm) any {\n\tptr := Object.(*int)\n\t*ptr += 1\n\treturn Object\n}\n\nvar n int\n\n// NOTE should be non-crossing\nfunc IncrGlobal() {\n\tn++\n}\n\n// TrySubOn attempts to mint a sub-realm token on a passed-in realm\n// value. Non-crossing (`_ int` discriminator), so the caller's cur\n// remains the topmost crossing cur — but borrow rule #1 runs this\n// body with m.Realm = crossrealm_b, so rlm.Sub must reject: a foreign\n// realm must not mint sub-identities in the caller's namespace.\nfunc TrySubOn(_ int, rlm realm) {\n\trlm.Sub(\"stolen\")\n}\n\n// TryBankerOnPrevious attempts to construct a RealmSend banker over the\n// CALLER via cur.Previous() (which is NOT IsCurrent). NewBanker must\n// reject it — otherwise a callee could set pkgAddr to the caller's\n// address and drain it through the pkgAddr==from gate. Regression guard\n// for the IsCurrent check in NewBanker.\nfunc TryBankerOnPrevious(cur realm) {\n\tbanker.NewBanker(banker.BankerTypeRealmSend, cur.Previous())\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/crossrealm_b\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"launderrvictim","path":"gno.land/r/tests/vm/launderrvictim","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/launderrvictim\"\ngno = \"0.9\"\n"},{"name":"launderrvictim.gno","body":"// Package launderrvictim is the /r/-DATA-DECLARED variant of the\n// launder-game victim. Its Immutable type is declared HERE (in /r/),\n// not in /p/launderpkg. This is the recommended inter-realm pattern:\n// realms declare their own logic data types.\n//\n// The hypothesis under test: with /r/-declared logic data, the\n// Attack H/I/J/K/L laundering shapes are structurally impossible.\n// Tests against this victim should all fail to mutate gImm.\npackage launderrvictim\n\nimport \"gno.land/p/demo/tests/launderpkg\"\n\n// Immutable is /r/-declared (the key difference from /r/laundervictim,\n// which uses /p/launderpkg.Immutable).\ntype Immutable struct {\n\tField string\n}\n\n// Read is /r/launderrvictim-declared, so calling it borrow rule #1 borrows\n// m.Realm to launderrvictim.\nfunc (i *Immutable) Read() string { return i.Field }\n\nvar gImm *Immutable\n\nfunc init() {\n\tgImm = \u0026Immutable{Field: \"rdata-original\"}\n}\n\n// GetImm hands out a pointer to gImm. Standard \"victim exposes a\n// pointer to its state\" antipattern — but with /r/-declared data,\n// the attacker should still be unable to write through it.\nfunc GetImm() *Immutable { return gImm }\n\n// ReadImm reads the current field for after-attack verification.\nfunc ReadImm() string { return gImm.Field }\n\n// UseAnyMutator boxes gImm as any and dispatches a /p/-declared\n// AnyMutator. This is the dangerous shape from Attack L: victim\n// boxes its own /r/-declared data through a /p/-defined interface\n// that the attacker can implement.\nfunc UseAnyMutator(m launderpkg.AnyMutator) {\n\tm.Run(gImm)\n}\n\n// ApplyHook dispatches a caller-supplied callback on gImm. The\n// callback's parameter type is /r/launderrvictim-declared, so /p/\n// packages can't supply this hook — only /r/ realms can.\nfunc ApplyHook(h func(*Immutable)) {\n\th(gImm)\n}\n\n// --- /p/-type embedded / fielded inside /r/-declared types ---\n//\n// The three shapes below mix /r/-declared containers with /p/-typed\n// inner state. Even though the container types are /r/-declared,\n// the inner /p/-typed values inherit /p/'s methods — including\n// Apply-style higher-order methods that take /p/-typed callbacks.\n// /p/-attacker code can supply such callbacks. The attacker reaches\n// the inner /p/-value (read access works), then invokes Apply with\n// a /p/-declared function pointer. Inside Apply (borrow rule #2ed\n// to /r/launderrvictim), the callback runs without any borrow rule #1 or #2\n// shift (top-level /p/ fn), so the write commits under victim\n// authority.\n\n// WithEmbed embeds launderpkg.Immutable by VALUE (method promotion\n// gives WithEmbed an .Apply method).\ntype WithEmbed struct {\n\tlaunderpkg.Immutable\n}\n\n// WithPtr has a POINTER FIELD to launderpkg.Immutable.\ntype WithPtr struct {\n\tInner *launderpkg.Immutable\n}\n\n// WithVal has a VALUE FIELD of launderpkg.Immutable (not embedded;\n// the field is named, no method promotion — but the value is still\n// addressable through c.Inner).\ntype WithVal struct {\n\tInner launderpkg.Immutable\n}\n\nvar (\n\tgWithEmbed *WithEmbed\n\tgWithPtr   *WithPtr\n\tgWithVal   *WithVal\n)\n\nfunc init() {\n\tgWithEmbed = \u0026WithEmbed{Immutable: launderpkg.Immutable{Field: \"embed-orig\"}}\n\tgWithPtr = \u0026WithPtr{Inner: \u0026launderpkg.Immutable{Field: \"ptr-orig\"}}\n\tgWithVal = \u0026WithVal{Inner: launderpkg.Immutable{Field: \"val-orig\"}}\n}\n\nfunc GetWithEmbed() *WithEmbed { return gWithEmbed }\nfunc GetWithPtr() *WithPtr     { return gWithPtr }\nfunc GetWithVal() *WithVal     { return gWithVal }\n\nfunc ReadEmbed() string { return gWithEmbed.Field }\nfunc ReadPtr() string   { return gWithPtr.Inner.Field }\nfunc ReadVal() string   { return gWithVal.Inner.Field }\n\n// --- Methods-less /p/-type inner state ---\n// launderpkg.Bare has no methods. These containers wrap Bare in\n// the three field shapes. The question: does readonly taint catch\n// a direct field write through the /r/-container's getter?\n\ntype WithBareEmbed struct {\n\tlaunderpkg.Bare\n}\n\ntype WithBarePtr struct {\n\tInner *launderpkg.Bare\n}\n\ntype WithBareVal struct {\n\tInner launderpkg.Bare\n}\n\nvar (\n\tgWithBareEmbed *WithBareEmbed\n\tgWithBarePtr   *WithBarePtr\n\tgWithBareVal   *WithBareVal\n)\n\nfunc init() {\n\tgWithBareEmbed = \u0026WithBareEmbed{Bare: launderpkg.Bare{Field: \"bare-embed-orig\"}}\n\tgWithBarePtr = \u0026WithBarePtr{Inner: \u0026launderpkg.Bare{Field: \"bare-ptr-orig\"}}\n\tgWithBareVal = \u0026WithBareVal{Inner: launderpkg.Bare{Field: \"bare-val-orig\"}}\n}\n\nfunc GetWithBareEmbed() *WithBareEmbed { return gWithBareEmbed }\nfunc GetWithBarePtr() *WithBarePtr     { return gWithBarePtr }\nfunc GetWithBareVal() *WithBareVal     { return gWithBareVal }\n\nfunc ReadBareEmbed() string { return gWithBareEmbed.Field }\nfunc ReadBarePtr() string   { return gWithBarePtr.Inner.Field }\nfunc ReadBareVal() string   { return gWithBareVal.Inner.Field }\n\n// --- Composite containers holding /p/-typed elements ---\n// Slices, arrays, maps of methods-less /p/-Bare values and pointers.\n\nvar (\n\tgBareSlice    []launderpkg.Bare\n\tgBarePtrSlice []*launderpkg.Bare\n\tgBareArr      [3]launderpkg.Bare\n\tgBarePtrArr   [3]*launderpkg.Bare\n\tgBareMap      map[string]launderpkg.Bare\n\tgBarePtrMap   map[string]*launderpkg.Bare\n)\n\nfunc init() {\n\tgBareSlice = []launderpkg.Bare{\n\t\t{Field: \"slice0\"}, {Field: \"slice1\"},\n\t}\n\tgBarePtrSlice = []*launderpkg.Bare{\n\t\t{Field: \"ptrslice0\"}, {Field: \"ptrslice1\"},\n\t}\n\tgBareArr = [3]launderpkg.Bare{\n\t\t{Field: \"arr0\"}, {Field: \"arr1\"}, {Field: \"arr2\"},\n\t}\n\tgBarePtrArr = [3]*launderpkg.Bare{\n\t\t{Field: \"ptrarr0\"}, {Field: \"ptrarr1\"}, {Field: \"ptrarr2\"},\n\t}\n\tgBareMap = map[string]launderpkg.Bare{\n\t\t\"a\": {Field: \"mapA\"}, \"b\": {Field: \"mapB\"},\n\t}\n\tgBarePtrMap = map[string]*launderpkg.Bare{\n\t\t\"a\": {Field: \"ptrmapA\"}, \"b\": {Field: \"ptrmapB\"},\n\t}\n}\n\nfunc GetBareSlice() []launderpkg.Bare            { return gBareSlice }\nfunc GetBarePtrSlice() []*launderpkg.Bare        { return gBarePtrSlice }\nfunc GetBareArr() *[3]launderpkg.Bare            { return \u0026gBareArr }\nfunc GetBarePtrArr() *[3]*launderpkg.Bare        { return \u0026gBarePtrArr }\nfunc GetBareMap() map[string]launderpkg.Bare     { return gBareMap }\nfunc GetBarePtrMap() map[string]*launderpkg.Bare { return gBarePtrMap }\n\nfunc ReadBareSlice0() string      { return gBareSlice[0].Field }\nfunc ReadBareSlice0Then1() string { return gBareSlice[1].Field }\nfunc ReadBarePtrSlice0() string   { return gBarePtrSlice[0].Field }\nfunc ReadBareArr0() string        { return gBareArr[0].Field }\nfunc ReadBarePtrArr0() string     { return gBarePtrArr[0].Field }\nfunc ReadBareMapA() string        { return gBareMap[\"a\"].Field }\nfunc ReadBarePtrMapA() string     { return gBarePtrMap[\"a\"].Field }\n\n// --- Panic/defer/recover helpers ---\n// These victim-side helpers expose scenarios where the m.Realm\n// borrow can interact with deferred calls, recover(), and panics\n// in unusual control-flow shapes.\n\n// DeferCallback installs h as a defer inside an /r/launderrvictim\n// frame, then returns. h runs at frame pop. The question: at the\n// time h is invoked, m.Realm has just been restored to caller's\n// realm by PopFrameAndReturn — but wait, defers run BEFORE\n// PopFrameAndReturn. So m.Realm should still be victim's. Does\n// the deferred h then run under victim authority?\nfunc DeferCallback(h func(*Immutable)) {\n\tdefer h(gImm)\n}\n\n// PanicAfterPushDefer pushes a defer and then panics, so the defer\n// runs as part of panic unwinding. Tests that m.Realm is correctly\n// borrowed when the defer body invokes a foreign function.\nfunc PanicAfterPushDefer(h func(*Immutable)) {\n\tdefer h(gImm)\n\tpanic(\"victim-induced panic\")\n}\n\n// DeferApplyHook defers an ApplyHook call. The deferred ApplyHook\n// itself runs borrow rule #1 to /r/launderrvictim, and inside the\n// callback runs as borrow rule #1 of the attacker's realm — the standard\n// known-open Apply pattern, but now triggered via defer.\nfunc DeferApplyHook(h func(*Immutable)) {\n\tdefer ApplyHook(h)\n}\n\n// RecoverAndRetry: inside a victim method, defer a recover, write\n// something to gImm, then panic. After the recover, the function\n// returns normally. Tests that internal panic/recover doesn't leak\n// state.\nfunc RecoverAndRetry(h func(*Immutable)) (recovered any) {\n\tdefer func() {\n\t\trecovered = recover()\n\t}()\n\th(gImm)\n\treturn\n}\n\n// CallThenPanic invokes h synchronously and then panics. If h is\n// attacker-supplied and writes via captured pointer, this is just\n// a re-shape of ApplyHook.\nfunc CallThenPanic(h func(*Immutable)) {\n\th(gImm)\n\tpanic(\"victim panic after callback\")\n}\n\n// CallPDeferApply: multi-level defer chain. Victim invokes a\n// /p/-method (DeferApply) on a victim-owned *launderpkg.Immutable;\n// the /p/-method defers the attacker callback. Three frames at\n// callback time: attacker.main → victim.CallPDeferApply →\n// /p/.DeferApply (deferred fn dispatches here).\nfunc CallPDeferApply(fn func(*launderpkg.Immutable)) {\n\tgWithPtr.Inner.DeferApply(fn)\n}\n\n// --- Stored-hook plumbing ---\n//\n// Victim accepts caller-registered callbacks and dispatches them\n// LATER, from inside a /r/-victim method body. If the registered\n// callback is /p/-declared and writes through a captured /r/-stamped\n// pointer, the laundering shape is: stored callback rather than\n// callback-arg.\n\ntype ImmHook func(*Immutable)\n\nvar gHooks []ImmHook\n\nfunc RegisterHook(h ImmHook) { gHooks = append(gHooks, h) }\nfunc RunHooks() {\n\tfor _, h := range gHooks {\n\t\th(gImm)\n\t}\n}\n\ntype PlainHook func()\n\nvar gPlainHooks []PlainHook\n\nfunc RegisterPlainHook(h PlainHook) { gPlainHooks = append(gPlainHooks, h) }\nfunc RunPlainHooks() {\n\tfor _, h := range gPlainHooks {\n\t\th()\n\t}\n}\n\nfunc ClearHooks() {\n\tgHooks = nil\n\tgPlainHooks = nil\n}\n\n// MakeWriterClosure constructs a /r/-victim-declared closure that\n// captures gImm and writes through it. The closure body is /r/-victim-\n// declared, so borrow rule #1 fires at invocation → m.Realm = /r/-victim →\n// write commits with victim authority. Returning this closure to an\n// attacker is \"consenting to write\" by the victim.\nfunc MakeWriterClosure(value string) func() {\n\treturn func() {\n\t\tgImm.Field = value\n\t}\n}\n\n// MakeApplyTrampoline returns a closure that captures \u0026gImm.Field\n// indirectly: it captures *Immutable, and dispatches a caller-supplied\n// callback fn on it. /r/-victim-declared body → borrow rule #1 → m.Realm =\n// /r/-victim. If `fn` is /p/-declared (e.g. EvilWrite), it inherits\n// victim authority. This is \"victim returns a closure that's itself\n// an Apply-style trampoline\" — a packaged Apply.\nfunc MakeApplyTrampoline() func(func(*Immutable)) {\n\treturn func(fn func(*Immutable)) {\n\t\tfn(gImm)\n\t}\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"test20","path":"gno.land/r/tests/vm/test20","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/test20\"\ngno = \"0.9\"\n"},{"name":"test20.gno","body":"// Package test20 implements a deliberately insecure ERC20 token for testing purposes.\n// The Test20 token allows anyone to mint any amount of tokens to any address, making\n// it unsuitable for production use. The primary goal of this package is to facilitate\n// testing and experimentation without any security measures or restrictions.\n//\n//\tWARNING: This token is highly insecure and should not be used in any\n//\t production environment. It is intended solely for testing and\n//\t educational purposes.\npackage test20\n\nimport (\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nvar (\n\tToken         *grc20.Token\n\tPrivateLedger *grc20.PrivateLedger\n)\n\nfunc init(cur realm) {\n\t// test20 only ever creates this one token, so id 0 can't collide.\n\tToken, PrivateLedger = grc20.NewToken(\"Test20\", \"TST\", 4, 0, cur)\n\tgrc20reg.Register(cross(cur), Token, \"\")\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"laundervictim","path":"gno.land/r/tests/vm/laundervictim","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/laundervictim\"\ngno = \"0.9\"\n"},{"name":"laundervictim.gno","body":"// Package laundervictim is the \"victim\" realm in the launder-game\n// tests. It exposes a package-level g that an attacker tries to\n// mutate by various means. Two shapes are exposed:\n//\n//   - gVal is `launderpkg.Object` (value type).\n//   - gPtr is `*launderpkg.Object` (pointer to a fresh Object).\n//\n// Both are allocated at init under this realm's context, so their\n// PkgID stamp is /r/.../laundervictim. The attacker's job is to make\n// the stamp not match m.Realm at the write site — by laundering the\n// stamp, capturing the value, or exploiting a borrow-rule shift.\npackage laundervictim\n\nimport \"gno.land/p/demo/tests/launderpkg\"\n\nvar (\n\tgVal launderpkg.Object\n\tgPtr *launderpkg.Object\n\t// gImm is an Immutable: same layout as Object but no mutator\n\t// method in /p/launderpkg. Victim exposes a pointer to it,\n\t// intending \"callers can read but not write.\"\n\tgImm *launderpkg.Immutable\n\t// gBuf is a victim-owned byte buffer (real, /r/laundervictim-stamped\n\t// after init). Used to probe whether a stdlib method (e.g.\n\t// base64.Encode) can be tricked into writing the victim's own buffer\n\t// when an attacker passes it as an out-parameter.\n\tgBuf []byte\n)\n\nfunc init() {\n\tgVal = launderpkg.Object{Field: \"original-val\"}\n\tgPtr = \u0026launderpkg.Object{Field: \"original-ptr\"}\n\tgImm = \u0026launderpkg.Immutable{Field: \"original-imm\"}\n\tgBuf = []byte(\"original-buffer!\")\n}\n\n// GetVal returns g by VALUE (caller gets a copy).\nfunc GetVal() launderpkg.Object { return gVal }\n\n// GetPtr returns the pointer to gPtr's underlying Object. The\n// returned pointer aliases the victim's persisted state.\nfunc GetPtr() *launderpkg.Object { return gPtr }\n\n// GetValAddr returns \u0026gVal — a pointer to the value-typed slot.\n// The returned pointer aliases the victim's persisted state.\nfunc GetValAddr() *launderpkg.Object { return \u0026gVal }\n\n// GetImm returns the pointer to gImm — a *Immutable, which has no\n// mutator method in /p/launderpkg. Victim's intent: callers can read\n// but not write.\nfunc GetImm() *launderpkg.Immutable { return gImm }\n\n// GetBuf returns the victim's own byte buffer. The returned slice\n// aliases the victim's persisted backing array (/r/laundervictim-stamped).\nfunc GetBuf() []byte { return gBuf }\n\n// ReadVal / ReadPtr / ReadImm / ReadBuf report the current values for\n// after-attack verification.\nfunc ReadVal() string { return gVal.Field }\nfunc ReadPtr() string { return gPtr.Field }\nfunc ReadImm() string { return gImm.Field }\nfunc ReadBuf() string { return string(gBuf) }\n\n// Exploiter is the interface the victim accepts. The attacker\n// supplies an implementation; the victim invokes Something(g)\n// passing its own g. This is the \"victim hands attacker the data\"\n// vector.\ntype Exploiter interface {\n\tSomething(launderpkg.Object)\n\tSomethingPtr(*launderpkg.Object)\n}\n\n// Invoke calls the attacker's methods passing the victim's g by both\n// value and by pointer.\nfunc Invoke(e Exploiter) {\n\te.Something(gVal)\n\te.SomethingPtr(gPtr)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"tests","path":"gno.land/p/demo/tests","files":[{"name":"README.md","body":"Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/demo/tests\"\ngno = \"0.9\"\n"},{"name":"tests.gno","body":"package tests\n\nimport (\n\t\"chain/runtime\"\n\t\"chain/runtime/unsafe\"\n\n\tpsubtests \"gno.land/p/demo/tests/subtests\"\n)\n\nconst World = \"world\"\n\nfunc CurrentRealmPath() string {\n\treturn unsafe.CurrentRealm().PkgPath()\n}\n\n//----------------------------------------\n// cross realm test vars\n\ntype TestRealmObject2 struct {\n\tField string\n}\n\nfunc (o2 *TestRealmObject2) Modify() {\n\to2.Field = \"modified\"\n}\n\n// Value-receiver mutator. By Go/Gno semantics this only mutates the\n// method's local copy. Used by readonly-taint filetests to probe\n// whether a receiver carrying the sticky N_Readonly bit propagates\n// the bit onto the method-frame copy.\nfunc (o2 TestRealmObject2) ModifyVal() {\n\to2.Field = \"modified-val\"\n}\n\n// Unexported mutator. Used by the method-expression visibility filetest:\n// `(*tests.TestRealmObject2).clearField` from outside this package must\n// be rejected at preprocess time.\nfunc (o2 *TestRealmObject2) clearField() {\n\to2.Field = \"\"\n}\n\nvar (\n\tsomevalue1 TestRealmObject2\n\tSomeValue2 TestRealmObject2\n\tSomeValue3 *TestRealmObject2\n)\n\nfunc init() {\n\tsomevalue1 = TestRealmObject2{Field: \"init\"}\n\tSomeValue2 = TestRealmObject2{Field: \"init\"}\n\tSomeValue3 = \u0026TestRealmObject2{Field: \"init\"}\n}\n\nfunc ModifyTestRealmObject2a() {\n\tsomevalue1.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2b() {\n\tSomeValue2.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2c() {\n\tSomeValue3.Field = \"modified\"\n}\n\nfunc GetPreviousRealm() runtime.Realm {\n\treturn unsafe.PreviousRealm()\n}\n\nfunc GetPSubtestsPreviousRealm() runtime.Realm {\n\treturn psubtests.GetPreviousRealm()\n}\n\n// Warning: unsafe pattern.\nfunc Exec(fn func()) {\n\tfn()\n}\n\n// ExecRlm mirrors Exec but threads the caller's rlm into the callback\n// so the callback can use `cross(rlm)` instead of bare `cross`.\nfunc ExecRlm(_ int, rlm realm, fn func(_ int, rlm realm)) {\n\tfn(0, rlm)\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"closuretest","path":"gno.land/r/demo/closuretest","files":[{"name":"closuretest.gno","body":"package closuretest\n\nimport \"strconv\"\n\nvar (\n\tcount   int\n\tstepper func() int\n)\n\nvar (\n\taccumulator func(int)\n\thistory     []int\n)\n\nfunc init() {\n\tstep := 3\n\tstepper = func() int {\n\t\tcount += step\n\t\treturn count\n\t}\n\n\tmaxLen := 10\n\thistory = make([]int, 0, maxLen)\n\taccumulator = func(val int) {\n\t\tif len(history) \u003c maxLen {\n\t\t\thistory = append(history, val)\n\t\t}\n\t}\n}\n\nfunc Step() string {\n\tresult := stepper()\n\treturn \"count=\" + strconv.Itoa(result)\n}\n\nfunc Accumulate(val int) string {\n\taccumulator(val)\n\treturn \"history length=\" + strconv.Itoa(len(history))\n}\n\nfunc Render(_ string) string {\n\treturn \"closuretest: count=\" + strconv.Itoa(count) + \" history=\" + strconv.Itoa(len(history))\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/closuretest\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"counter","path":"gno.land/r/demo/counter","files":[{"name":"counter.gno","body":"package counter\n\nimport \"strconv\"\n\nvar counter int\n\nfunc Increment(_ realm) int {\n\tcounter++\n\treturn counter\n}\n\nfunc Render(_ string) string {\n\treturn strconv.Itoa(counter)\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/counter\"\ngno = \"0.9\"\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"subhost","path":"gno.land/r/tests/vm/subhost","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/tests/vm/subhost\"\ngno = \"0.9\"\n"},{"name":"subhost.gno","body":"// Package subhost is a test fixture for realm.Sub sub-identity\n// presentation across a real package boundary. It plays two roles:\n//\n//   - Host: ActAsSub mints a sub of its OWN live cur and crosses into an\n//     observer with it.\n//   - Observer: Observe reads the presented identity (cur.Previous) that\n//     a caller crossed in with, and asserts unsafe.PreviousRealm parity.\n//\n// Consumers:\n//   - gnovm/tests/files/zrealm_sub_foreign_present.gno crosses into\n//     Observe directly with a sub minted in the caller's own realm, to\n//     check cross-realm identity presentation + Subpath() through a cross.\n//   - gno.land/pkg/integration/testdata/subrealm_run_parity.txtar enters\n//     via MsgRun, crosses into ActAsSub, which mints a sub and crosses\n//     into Observe — the MsgRun-entry counterpart to the MsgCall-entry\n//     parity filetest zrealm_sub1.gno.\npackage subhost\n\nimport (\n\t\"chain\"\n\t\"chain/runtime/unsafe\"\n)\n\nconst pkgPath = \"gno.land/r/tests/vm/subhost\"\n\n// Observe reports what the immediate caller presented as its identity.\n// The three cur.Previous() views (address, synthesized pkgpath, subpath)\n// are the callee-side surface a sub-token must present; the two parity\n// booleans assert unsafe.PreviousRealm() reads the SAME identity (v2\n// §5.4: unsafe.* must agree with cur.Previous()). prevParity is the\n// load-bearing assertion for the identity-chain walk; curParity guards\n// height 0.\nfunc Observe(cur realm) {\n\tprev := cur.Previous()\n\tprintln(\"observed prev.PkgPath:\", prev.PkgPath())\n\tprintln(\"observed prev.Subpath:\", prev.Subpath())\n\tprintln(\"observed prev.IsUserCall:\", prev.IsUserCall())\n\t// Short-circuit the primary case FIRST: DerivePkgSubAddr panics on an\n\t// empty subpath, so it must not be evaluated when prev is a primary.\n\tprintln(\"prev.Address == DerivePkgSubAddr(host, subpath):\",\n\t\tprev.Subpath() == \"\" ||\n\t\t\tstring(prev.Address()) == string(chain.DerivePkgSubAddr(pkgPathOf(prev.PkgPath()), prev.Subpath())))\n\tprintln(\"unsafe prev parity:\",\n\t\tstring(prev.Address()) == string(unsafe.PreviousRealm().Address()) \u0026\u0026\n\t\t\tprev.PkgPath() == unsafe.PreviousRealm().PkgPath())\n\tprintln(\"unsafe cur parity:\",\n\t\tstring(cur.Address()) == string(unsafe.CurrentRealm().Address()) \u0026\u0026\n\t\t\tcur.PkgPath() == unsafe.CurrentRealm().PkgPath())\n}\n\n// pkgPathOf returns the host portion of a possibly-synthesized pkgpath,\n// so DerivePkgSubAddr (which takes the host) can be recomputed from the\n// callee's observation alone.\nfunc pkgPathOf(p string) string {\n\thost, _, _ := chain.SplitPkgSubPath(p)\n\treturn host\n}\n\n// ActAsSub mints a sub-identity of subhost's OWN live cur and crosses\n// with it into Observe. Used by the MsgRun-entry parity test: the\n// outermost entry is a `/e/\u003caddr\u003e/run` ephemeral, but subhost's cur is a\n// first-class primary here, so cur.Sub is legal and the sub presents to\n// Observe exactly as in the MsgCall path.\nfunc ActAsSub(cur realm, subpath string) {\n\tsub := cur.Sub(subpath)\n\tObserve(cross(sub))\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"bylaws","path":"gno.land/p/nt/bylaws/v0","files":[{"name":"bylaws.gno","body":"// Package bylaws stores a DAO's governing documents — bylaws and\n// mandates — as named plaintext files and amends them with verifiable\n// diff patches.\n//\n// Documents are keyed by a slash-separated path (\"mandates/treasury.md\");\n// folders are a naming convention over the path, not stored objects — a\n// folder exists exactly when a document path has it as a prefix. The only\n// mutation is Apply: a Patch carries the sha256 of the document text it\n// was diffed against plus the edit script that transforms that text into\n// the proposed one. Apply rejects the patch when the document has changed\n// since (optimistic concurrency, no clobbering) and otherwise replays the\n// script. An amendment whose result is empty removes the document, so a\n// stored document is never empty.\n//\n// The package is governance-agnostic: it decides nothing about WHO may\n// amend. A consuming realm (e.g. a DAO) gates Apply behind its own vote\n// and keeps the *Bylaws handle private — Apply mutates, so the handle\n// must never be exposed to untrusted callers.\npackage bylaws\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/bptree/v0\"\n)\n\nconst (\n\t// MaxPathLen bounds a document path's byte length.\n\tMaxPathLen = 200\n\n\t// MaxDocLen bounds a document's byte length. Bylaws are human-written\n\t// prose; the cap keeps documents renderable and patch replay bounded.\n\tMaxDocLen = 64 * 1024\n)\n\nvar (\n\tErrInvalidPath  = errors.New(\"bylaws: invalid document path\")\n\tErrInvalidPatch = errors.New(\"bylaws: invalid patch\")\n\tErrInvalidText  = errors.New(\"bylaws: text is not valid UTF-8\")\n\tErrStalePatch   = errors.New(\"bylaws: document changed since the patch base\")\n\tErrDocTooLarge  = errors.New(\"bylaws: document exceeds the maximum size\")\n)\n\n// Bylaws is one DAO's set of governing documents, keyed by path.\ntype Bylaws struct {\n\tdocs *bptree.BPTree // path (string) -\u003e document text (string, never empty)\n}\n\n// New creates an empty document set.\nfunc New() *Bylaws {\n\treturn \u0026Bylaws{docs: bptree.NewBPTree32()}\n}\n\n// Get returns a document's text and whether it exists.\nfunc (b *Bylaws) Get(path string) (string, bool) {\n\tif v := b.docs.Get(path); v != nil {\n\t\treturn v.(string), true\n\t}\n\treturn \"\", false\n}\n\n// Has reports whether a document exists.\nfunc (b *Bylaws) Has(path string) bool {\n\treturn b.docs.Has(path)\n}\n\n// Size returns the number of documents.\nfunc (b *Bylaws) Size() int {\n\treturn b.docs.Size()\n}\n\n// Hash returns the hex sha256 of a document's text, or an empty string\n// when the document does not exist. It is the base a Patch must pin to\n// amend the document (an empty hash pins \"the document must not exist\").\nfunc (b *Bylaws) Hash(path string) string {\n\tif text, ok := b.Get(path); ok {\n\t\treturn HashText(text)\n\t}\n\treturn \"\"\n}\n\n// List returns the sorted document paths under a prefix. An empty prefix\n// lists every document. The prefix is a raw path prefix: include the\n// trailing slash to scope to a folder (e.g. \"mandates/\"), or \"mandates\"\n// also matches a sibling file like \"mandates-old.md\".\nfunc (b *Bylaws) List(prefix string) []string {\n\tpaths := []string{}\n\tb.Iterate(prefix, func(path, _ string) bool {\n\t\tpaths = append(paths, path)\n\t\treturn false\n\t})\n\treturn paths\n}\n\n// Iterate walks the documents under a prefix in sorted path order until\n// fn returns true. It returns true when the walk was stopped by fn. The\n// set must not be amended during iteration (no Apply from fn).\nfunc (b *Bylaws) Iterate(prefix string, fn func(path, text string) bool) bool {\n\tend := \"\"\n\tif prefix != \"\" {\n\t\t// Path bytes are all \u003c 0x7f (see IsValidPath), so every key with\n\t\t// the prefix sorts before prefix+\"\\x7f\". The tree iterates the\n\t\t// half-open range [start, end) in sorted key order.\n\t\tend = prefix + \"\\x7f\"\n\t}\n\treturn b.docs.Iterate(prefix, end, func(key string, value any) bool {\n\t\treturn fn(key, value.(string))\n\t})\n}\n\n// HashText returns the hex sha256 of a text.\nfunc HashText(text string) string {\n\tsum := sha256.Sum256([]byte(text))\n\treturn hex.EncodeToString(sum[:])\n}\n\n// IsValidPath reports whether a path names a document: one or more\n// non-empty \"/\"-separated segments of [a-zA-Z0-9._-] characters, where no\n// segment is \".\" or \"..\". The restricted charset keeps paths render- and\n// link-safe and the patch encoding delimiter-free.\nfunc IsValidPath(path string) bool {\n\tif path == \"\" || len(path) \u003e MaxPathLen {\n\t\treturn false\n\t}\n\tsegStart := 0\n\tfor i := 0; i \u003c= len(path); i++ {\n\t\tif i == len(path) || path[i] == '/' {\n\t\t\tseg := path[segStart:i]\n\t\t\tif seg == \"\" || seg == \".\" || seg == \"..\" {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tsegStart = i + 1\n\t\t\tcontinue\n\t\t}\n\t\tif !isPathChar(path[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isPathChar(c byte) bool {\n\treturn c \u003e= 'a' \u0026\u0026 c \u003c= 'z' ||\n\t\tc \u003e= 'A' \u0026\u0026 c \u003c= 'Z' ||\n\t\tc \u003e= '0' \u0026\u0026 c \u003c= '9' ||\n\t\tc == '.' || c == '_' || c == '-'\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/nt/bylaws/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"},{"name":"patch.gno","body":"package bylaws\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"gno.land/p/onbloc/diff\"\n)\n\n// MaxOps bounds the number of ops a decoded patch may carry. DiffTexts\n// output always stays far below it (see maxMyersRunes), so the cap only\n// rejects hand-built pathological payloads at the wire boundary.\nconst MaxOps = 16 * 1024\n\n// maxMyersRunes bounds the combined rune length of the texts handed to\n// MyersDiff, whose memory is O((N+M)·D): past the budget, DiffTexts\n// falls back to replacing the whole changed region in one delete+insert\n// (payload ~ proposed size; replay stays linear). The common prefix and\n// suffix are trimmed first, so ordinary human edits — small changes in a\n// large document — stay within budget and get a minimal script.\nconst maxMyersRunes = 1024\n\n// OpType is the kind of a patch operation.\ntype OpType byte\n\nconst (\n\tOpKeep   OpType = 'K' // keep the next N runes of the base text\n\tOpDelete OpType = 'D' // delete the next N runes of the base text\n\tOpInsert OpType = 'I' // insert literal text\n)\n\n// Op is one run of a patch's edit script. Keep and Delete address the\n// base text positionally (a rune count), so only Insert carries bytes —\n// a small edit to a large document stays a small patch.\ntype Op struct {\n\tType OpType\n\tN    int    // rune count (Keep and Delete only)\n\tText string // inserted literal (Insert only)\n}\n\n// Patch is a verifiable amendment to one document: the edit script that\n// transforms the document's base text into the proposed text, pinned to\n// that base by hash. Apply rejects the patch unless the target currently\n// hashes to Base, so a patch can never be applied to text it was not\n// diffed against.\ntype Patch struct {\n\tPath string // target document path\n\tBase string // hex sha256 of the base text; \"\" pins \"document absent\" (create)\n\tOps  []Op   // edit script, replayed in order against the base text\n}\n\n// IsCreate reports whether the patch creates the document (its base pins\n// \"document absent\").\nfunc (p Patch) IsCreate() bool {\n\treturn p.Base == \"\"\n}\n\n// IsRemove reports whether applying the patch removes the document (the\n// script deletes the whole base text and inserts nothing).\nfunc (p Patch) IsRemove() bool {\n\tif len(p.Ops) == 0 {\n\t\treturn false\n\t}\n\tfor _, op := range p.Ops {\n\t\tif op.Type != OpDelete {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// IsNoop reports whether applying the patch leaves the document set\n// unchanged (the script only keeps text, or creates an empty document).\n// The check is shape-based: a hand-built script that deletes and\n// reinserts identical text is not detected (Diff never produces one).\nfunc (p Patch) IsNoop() bool {\n\tfor _, op := range p.Ops {\n\t\tif op.Type != OpKeep {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Diff builds the patch that changes the document at path to the\n// proposed text: it diffs against the document's current text and pins\n// its hash. A new path yields a create patch; empty proposed text yields\n// a remove patch.\nfunc (b *Bylaws) Diff(path, proposed string) (Patch, error) {\n\tcur, exists := b.Get(path)\n\treturn DiffTexts(path, cur, proposed, exists)\n}\n\n// DiffTexts builds the patch transforming base into proposed for the\n// document at path. exists reports whether the document currently exists\n// (base must be \"\" when it does not); a patch built with exists=false\n// creates the document. Both texts must be valid UTF-8 — documents are\n// plaintext, and the invariant keeps Keep/Delete rune math byte-faithful.\nfunc DiffTexts(path, base, proposed string, exists bool) (Patch, error) {\n\tif !IsValidPath(path) {\n\t\treturn Patch{}, ErrInvalidPath\n\t}\n\tif len(proposed) \u003e MaxDocLen {\n\t\treturn Patch{}, ErrDocTooLarge\n\t}\n\tif !utf8.ValidString(base) || !utf8.ValidString(proposed) {\n\t\treturn Patch{}, ErrInvalidText\n\t}\n\tbaseHash := \"\"\n\tif exists {\n\t\tbaseHash = HashText(base)\n\t}\n\treturn Patch{\n\t\tPath: path,\n\t\tBase: baseHash,\n\t\tOps:  diffOps(base, proposed),\n\t}, nil\n}\n\n// diffOps builds the coalesced edit script from base to proposed: trim\n// the common prefix and suffix, Myers-diff the differing middles, and\n// past the maxMyersRunes budget replace the whole middle instead. The\n// trimmed middles differ at both ends (or are empty), so the pieces\n// never need merging with the surrounding Keep ops.\nfunc diffOps(base, proposed string) []Op {\n\tb, p := []rune(base), []rune(proposed)\n\n\tpre := 0\n\tfor pre \u003c len(b) \u0026\u0026 pre \u003c len(p) \u0026\u0026 b[pre] == p[pre] {\n\t\tpre++\n\t}\n\tsuf := 0\n\tfor suf \u003c len(b)-pre \u0026\u0026 suf \u003c len(p)-pre \u0026\u0026 b[len(b)-1-suf] == p[len(p)-1-suf] {\n\t\tsuf++\n\t}\n\tbMid, pMid := b[pre:len(b)-suf], p[pre:len(p)-suf]\n\n\tops := []Op{}\n\tif pre \u003e 0 {\n\t\tops = append(ops, Op{Type: OpKeep, N: pre})\n\t}\n\tif len(bMid)+len(pMid) \u003e maxMyersRunes {\n\t\tif len(bMid) \u003e 0 {\n\t\t\tops = append(ops, Op{Type: OpDelete, N: len(bMid)})\n\t\t}\n\t\tif len(pMid) \u003e 0 {\n\t\t\tops = append(ops, Op{Type: OpInsert, Text: string(pMid)})\n\t\t}\n\t} else {\n\t\tops = append(ops, coalesce(diff.MyersDiff(string(bMid), string(pMid)))...)\n\t}\n\tif suf \u003e 0 {\n\t\tops = append(ops, Op{Type: OpKeep, N: suf})\n\t}\n\treturn ops\n}\n\n// coalesce collapses a per-rune Myers edit script into run-length ops:\n// runs of Keep/Delete become counts, runs of Insert carry their literal.\nfunc coalesce(edits []diff.Edit) []Op {\n\tops := []Op{}\n\tvar (\n\t\tlit     strings.Builder\n\t\tcount   int\n\t\tcurType OpType\n\t\thave    bool\n\t)\n\tflush := func() {\n\t\tif !have {\n\t\t\treturn\n\t\t}\n\t\tif curType == OpInsert {\n\t\t\tops = append(ops, Op{Type: OpInsert, Text: lit.String()})\n\t\t} else {\n\t\t\tops = append(ops, Op{Type: curType, N: count})\n\t\t}\n\t\tlit.Reset()\n\t\tcount = 0\n\t\thave = false\n\t}\n\tfor _, e := range edits {\n\t\tvar t OpType\n\t\tswitch e.Type {\n\t\tcase diff.EditKeep:\n\t\t\tt = OpKeep\n\t\tcase diff.EditInsert:\n\t\t\tt = OpInsert\n\t\tdefault:\n\t\t\tt = OpDelete\n\t\t}\n\t\tif !have || t != curType {\n\t\t\tflush()\n\t\t\tcurType = t\n\t\t\thave = true\n\t\t}\n\t\tif t == OpInsert {\n\t\t\tlit.WriteRune(e.Char)\n\t\t} else {\n\t\t\tcount++\n\t\t}\n\t}\n\tflush()\n\treturn ops\n}\n\n// Apply verifies the patch and amends the document set: the target's\n// current text must hash to the patch base (\"\" base means the document\n// must not exist), and the edit script must consume exactly that text.\n// An empty result removes the document. Apply is the package's only\n// mutation; on any error the set is unchanged.\nfunc (b *Bylaws) Apply(p Patch) error {\n\tif !IsValidPath(p.Path) {\n\t\treturn ErrInvalidPath\n\t}\n\tcur, exists := b.Get(p.Path)\n\tcurHash := \"\"\n\tif exists {\n\t\tcurHash = HashText(cur)\n\t}\n\tif p.Base != curHash {\n\t\treturn ErrStalePatch\n\t}\n\tout, err := replay(cur, p.Ops)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(out) \u003e MaxDocLen {\n\t\treturn ErrDocTooLarge\n\t}\n\tif out == \"\" {\n\t\tb.docs.Remove(p.Path)\n\t\treturn nil\n\t}\n\tb.docs.Set(p.Path, out)\n\treturn nil\n}\n\n// replay runs the edit script against the base text: Keep emits base\n// runes and advances, Delete advances, Insert emits its literal. The\n// script must consume the base exactly, so a script that does not fit\n// the text it runs against fails instead of producing garbage.\nfunc replay(base string, ops []Op) (string, error) {\n\tr := []rune(base)\n\tvar (\n\t\tsb strings.Builder\n\t\ti  int\n\t)\n\tfor _, op := range ops {\n\t\tswitch op.Type {\n\t\tcase OpKeep:\n\t\t\tif op.N \u003c= 0 || op.N \u003e len(r)-i {\n\t\t\t\treturn \"\", ErrInvalidPatch\n\t\t\t}\n\t\t\tsb.WriteString(string(r[i : i+op.N]))\n\t\t\ti += op.N\n\t\tcase OpDelete:\n\t\t\tif op.N \u003c= 0 || op.N \u003e len(r)-i {\n\t\t\t\treturn \"\", ErrInvalidPatch\n\t\t\t}\n\t\t\ti += op.N\n\t\tcase OpInsert:\n\t\t\tif op.Text == \"\" {\n\t\t\t\treturn \"\", ErrInvalidPatch\n\t\t\t}\n\t\t\tsb.WriteString(op.Text)\n\t\tdefault:\n\t\t\treturn \"\", ErrInvalidPatch\n\t\t}\n\t}\n\tif i != len(r) {\n\t\treturn \"\", ErrInvalidPatch\n\t}\n\treturn sb.String(), nil\n}\n\n// Format renders the patch against its base text as a plain-text change\n// summary: kept runs collapse to a marker, deleted and inserted text is\n// shown literally with every line marker-prefixed (\"- \"/\"+ \"), so a\n// multi-line literal cannot masquerade as the summary's own markers (an\n// insertion containing \"\\n- fake\" renders as \"+ …\" and \"+ - fake\"). It\n// fails like replay when the script does not fit the base. The output is\n// raw text — callers rendering markdown must escape it (the content is\n// document text, and insertions are proposer-controlled).\nfunc (p Patch) Format(base string) (string, error) {\n\tr := []rune(base)\n\tvar (\n\t\tsb strings.Builder\n\t\ti  int\n\t)\n\tfor _, op := range p.Ops {\n\t\tswitch op.Type {\n\t\tcase OpKeep:\n\t\t\tif op.N \u003c= 0 || op.N \u003e len(r)-i {\n\t\t\t\treturn \"\", ErrInvalidPatch\n\t\t\t}\n\t\t\tsb.WriteString(\"= \" + strconv.Itoa(op.N) + \" unchanged\\n\")\n\t\t\ti += op.N\n\t\tcase OpDelete:\n\t\t\tif op.N \u003c= 0 || op.N \u003e len(r)-i {\n\t\t\t\treturn \"\", ErrInvalidPatch\n\t\t\t}\n\t\t\twriteMarked(\u0026sb, \"- \", string(r[i:i+op.N]))\n\t\t\ti += op.N\n\t\tcase OpInsert:\n\t\t\tif op.Text == \"\" {\n\t\t\t\treturn \"\", ErrInvalidPatch\n\t\t\t}\n\t\t\twriteMarked(\u0026sb, \"+ \", op.Text)\n\t\tdefault:\n\t\t\treturn \"\", ErrInvalidPatch\n\t\t}\n\t}\n\tif i != len(r) {\n\t\treturn \"\", ErrInvalidPatch\n\t}\n\treturn sb.String(), nil\n}\n\n// writeMarked writes text with every line prefixed by marker.\nfunc writeMarked(sb *strings.Builder, marker, text string) {\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tsb.WriteString(marker)\n\t\tsb.WriteString(line)\n\t\tsb.WriteByte('\\n')\n\t}\n}\n\n// Encode serializes the patch to a compact single-string payload fit for\n// a transaction argument: \"v0:\u003cpath\u003e:\u003cbase\u003e:\" followed by one segment\n// per op — \"K\u003cn\u003e;\" and \"D\u003cn\u003e;\" carry rune counts, \"I\u003clen\u003e:\u003cbytes\u003e;\"\n// carries the inserted literal length-prefixed by its byte length (no\n// escaping needed). DecodePatch is the exact inverse.\nfunc (p Patch) Encode() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"v0:\")\n\tsb.WriteString(p.Path)\n\tsb.WriteByte(':')\n\tsb.WriteString(p.Base)\n\tsb.WriteByte(':')\n\tfor _, op := range p.Ops {\n\t\tswitch op.Type {\n\t\tcase OpKeep, OpDelete:\n\t\t\tsb.WriteByte(byte(op.Type))\n\t\t\tsb.WriteString(strconv.Itoa(op.N))\n\t\tcase OpInsert:\n\t\t\tsb.WriteByte(byte(OpInsert))\n\t\t\tsb.WriteString(strconv.Itoa(len(op.Text)))\n\t\t\tsb.WriteByte(':')\n\t\t\tsb.WriteString(op.Text)\n\t\t}\n\t\tsb.WriteByte(';')\n\t}\n\treturn sb.String()\n}\n\n// DecodePatch parses an Encode payload back into a Patch, validating the\n// path, the base hash shape, and every op (counts must be canonical, so\n// DecodePatch accepts exactly Encode's output shape; insert literals\n// must be valid UTF-8, keeping documents plaintext and Keep's rune math\n// byte-faithful). Replay validity against the actual document is Apply's\n// job; DecodePatch only guarantees the patch is well-formed.\nfunc DecodePatch(s string) (Patch, error) {\n\tif !strings.HasPrefix(s, \"v0:\") {\n\t\treturn Patch{}, ErrInvalidPatch\n\t}\n\trest := s[len(\"v0:\"):]\n\ti := strings.IndexByte(rest, ':')\n\tif i \u003c 0 {\n\t\treturn Patch{}, ErrInvalidPatch\n\t}\n\tpath := rest[:i]\n\trest = rest[i+1:]\n\tif !IsValidPath(path) {\n\t\treturn Patch{}, ErrInvalidPath\n\t}\n\tj := strings.IndexByte(rest, ':')\n\tif j \u003c 0 {\n\t\treturn Patch{}, ErrInvalidPatch\n\t}\n\tbase := rest[:j]\n\trest = rest[j+1:]\n\tif !isValidBase(base) {\n\t\treturn Patch{}, ErrInvalidPatch\n\t}\n\tops := []Op{}\n\tfor len(rest) \u003e 0 {\n\t\tif len(ops) == MaxOps {\n\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t}\n\t\tt := OpType(rest[0])\n\t\trest = rest[1:]\n\t\tswitch t {\n\t\tcase OpKeep, OpDelete:\n\t\t\tk := strings.IndexByte(rest, ';')\n\t\t\tif k \u003c 0 {\n\t\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t\t}\n\t\t\tn, ok := parseCount(rest[:k])\n\t\t\tif !ok {\n\t\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t\t}\n\t\t\tops = append(ops, Op{Type: t, N: n})\n\t\t\trest = rest[k+1:]\n\t\tcase OpInsert:\n\t\t\tk := strings.IndexByte(rest, ':')\n\t\t\tif k \u003c 0 {\n\t\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t\t}\n\t\t\tn, ok := parseCount(rest[:k])\n\t\t\tif !ok {\n\t\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t\t}\n\t\t\trest = rest[k+1:]\n\t\t\tif len(rest) \u003c n+1 || rest[n] != ';' {\n\t\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t\t}\n\t\t\ttext := rest[:n]\n\t\t\tif !utf8.ValidString(text) {\n\t\t\t\treturn Patch{}, ErrInvalidText\n\t\t\t}\n\t\t\tops = append(ops, Op{Type: OpInsert, Text: text})\n\t\t\trest = rest[n+1:]\n\t\tdefault:\n\t\t\treturn Patch{}, ErrInvalidPatch\n\t\t}\n\t}\n\treturn Patch{Path: path, Base: base, Ops: ops}, nil\n}\n\n// parseCount parses a strictly canonical op count: decimal digits with\n// no sign and no leading zero (so Encode∘DecodePatch is the identity on\n// accepted payloads), in (0, MaxDocLen].\nfunc parseCount(s string) (int, bool) {\n\tif s == \"\" || s[0] == '0' || len(s) \u003e 6 {\n\t\treturn 0, false\n\t}\n\tn := 0\n\tfor i := 0; i \u003c len(s); i++ {\n\t\tc := s[i]\n\t\tif c \u003c '0' || c \u003e '9' {\n\t\t\treturn 0, false\n\t\t}\n\t\tn = n*10 + int(c-'0')\n\t}\n\tif n \u003e MaxDocLen {\n\t\treturn 0, false\n\t}\n\treturn n, true\n}\n\n// isValidBase reports whether a base is the empty sentinel (create) or a\n// 64-char lowercase hex sha256.\nfunc isValidBase(base string) bool {\n\tif base == \"\" {\n\t\treturn true\n\t}\n\tif len(base) != 64 {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(base); i++ {\n\t\tc := base[i]\n\t\tif !(c \u003e= '0' \u0026\u0026 c \u003c= '9' || c \u003e= 'a' \u0026\u0026 c \u003c= 'f') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"names","path":"gno.land/r/sys/names","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/names\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"render.gno","body":"package names\n\nfunc Render(_ string) string {\n\treturn `# r/sys/names\nSystem Realm for checking namespace deployment permissions.`\n}\n"},{"name":"verifier.gno","body":"// Package names enforces namespace permissions for package deployment.\n//\n// Two namespace shapes grant deploy authority when enforcement is enabled:\n//\n//  1. PA (personal-address) namespaces — gno.land/{r,p}/\u003caddr\u003e/* — the\n//     deployer's address string equals the namespace literal. Anyone can\n//     deploy under their own address.\n//\n//  2. Registered-name namespaces — gno.land/{r,p}/\u003cname\u003e/* — r/sys/users\n//     has a (name → addr) mapping where the resolved address equals the\n//     deployer AND the name is the user's CURRENT name (not a historical\n//     alias from a rename chain). This is the bridge that lets\n//     r/sys/namereg/v1 (or any other DAO-whitelisted controller) grant\n//     deploy authority via name registration.\n//\n// Authority is unscoped: a registered name owns BOTH r/\u003cname\u003e/* and\n// p/\u003cname\u003e/* paths. There is no sub-prefix isolation (e.g. r/u/\u003cname\u003e/*).\n//\n// The realm exposes an emergency-halt switch via SetPaused. When paused,\n// the verifier rejects EVERY namespace check — PA included — until\n// unpaused. This is the \"true emergency\" semantic; the narrow alternative\n// (pause registered-name only, preserve PA) was considered and rejected\n// because the threats most likely to justify pausing this realm\n// (verifier bug, compromised controller, signature-layer incident) do\n// not reliably exempt PA from the same blast radius.\npackage names\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/r/gov/dao\"\n\tgovimpl \"gno.land/r/gov/dao/v3/impl\"\n\tmemberstore \"gno.land/r/gov/dao/v3/memberstore\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nvar (\n\t// admin is the GovDAO T1 multisig address, hardcoded at realm-source\n\t// commit time. Its only capability is gating Enable() — a one-way,\n\t// one-shot genesis activation of the namespace verifier. The address\n\t// has no other authority on this realm; pause/unpause is gated on a\n\t// separate GovDAO T1 proposal (see ProposeSetPaused), and there is\n\t// no SetEnabled(false) or SetAdmin path.\n\t//\n\t// Hardcoding is acceptable because:\n\t//   - Enable() is called once, at chain genesis. After that the\n\t//     address is dead weight — no further capability flows through it.\n\t//   - The narrow blast radius of \"stale admin\" is \"Enable() can never\n\t//     be called\", which leaves the verifier in pre-Enable bypass mode\n\t//     (returns true for all checks) — degraded but not exploitable.\n\t//   - A rotation path would only matter if Enable() needed to be\n\t//     re-issued. It doesn't; the flag is sticky.\n\t//\n\t// If the genesis activation pattern ever needs to change (e.g. a\n\t// SetEnabled(false) emergency disable is added later), this admin\n\t// model should be replaced with a GovDAO T1 proposal flow that\n\t// mirrors ProposeSetPaused. Until then, the hardcoded address is\n\t// the smallest viable governance surface for the one-shot use case.\n\tadmin   = address(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\")\n\tenabled = false\n\tpaused  = false\n)\n\n// nameLookupFn returns (addr, ok) for a given registered name. Allows\n// the verifier function to be unit-tested without wiring up r/sys/users\n// state — production binds resolveCurrentName as the lookup, tests pass\n// nil (PA-only) or a fake.\ntype nameLookupFn func(name string) (addr address, ok bool)\n\n// IsAuthorizedAddressForNamespace checks if the given address can deploy\n// to the given namespace. See package doc for the two authorization paths\n// and the pause semantic.\n//\n// Pre-Enable, all checks pass (testing/dev convenience).\nfunc IsAuthorizedAddressForNamespace(address_XXX address, namespace string) bool {\n\treturn verifier(enabled, paused, address_XXX, namespace, resolveCurrentName)\n}\n\n// resolveCurrentName is the production nameLookupFn, backed by r/sys/users.\n// Returns ok=true only if the name resolves to a non-deleted user AND the\n// queried name is that user's CURRENT name (the most recent UpdateName).\n//\n// Restricting to the current name has two consequences worth knowing:\n//\n//  1. After a UpdateName from \"alice\" to \"alice2\", the user keeps deploy\n//     authority over r/alice2/* but LOSES it for r/alice/*. Already-\n//     deployed packages at r/alice/* keep working — deploy-time\n//     authorization doesn't unwind the past — but no NEW deploys can\n//     land there.\n//\n//  2. The old name \"alice\" is also unregisterable by anyone else: when\n//     r/sys/users.UpdateName runs, it inserts the new name into nameStore\n//     but does not remove the old one. r/sys/users.RegisterUser then\n//     rejects re-registration of \"alice\" with ErrNameTaken. Net effect:\n//     a rename permanently removes the old name from circulation.\n//\n// The alternative (allow historical aliases to retain authority) was\n// rejected because it lets a single user register one cheap name, then\n// rename N times to claim authority over N distinct namespaces — a\n// stealth namespace acquisition vector worse than the current\n// burn-on-rename behavior.\nfunc resolveCurrentName(name string) (address, bool) {\n\tdata, isCurrent := susers.ResolveName(name)\n\tif data == nil || !isCurrent {\n\t\treturn \"\", false\n\t}\n\treturn data.Addr(), true\n}\n\n// Enable enables the namespace check for this realm.\n// The namespace check is disabled initially to ease txtar and other testing contexts,\n// but this function is meant to be called in the genesis of a chain.\nfunc Enable(cur realm) {\n\tif cur.Previous().Address() != admin {\n\t\tpanic(\"caller is not admin\")\n\t}\n\tenabled = true\n}\n\nfunc IsEnabled() bool {\n\treturn enabled\n}\n\n// ProposeSetPaused returns a GovDAO proposal request that, when voted\n// through and executed, toggles the chain-wide deploy gate. When the\n// realm is paused, the verifier rejects EVERY namespace check — PA\n// (personal-address) included — until a subsequent ProposeSetPaused(false)\n// proposal executes.\n//\n// This is an emergency halt. A paused state means NO new MsgAddPackage\n// transactions land at any path on the chain. Existing realms continue\n// to receive MsgCall traffic normally — pause is scoped to addpkg, not\n// to all VM operations. Use cases:\n//   - Bug discovered in this realm or r/sys/users that requires a\n//     hotfix before further deploys can be trusted.\n//   - Wallet/signature-layer incident under investigation.\n//\n// (A \"compromised controller\" use case was considered and removed: the\n// controller's RegisterUser path is direct into r/sys/users and does\n// NOT go through this verifier, so pause does not freeze new\n// registrations. To contain a compromised controller, the appropriate\n// flow is ProposeControllerRemoval in r/sys/users, not pause here.)\n//\n// The narrow alternative (pause registered-name path only, preserve\n// PA) was considered and rejected. See package doc for rationale.\n//\n// Gated on GovDAO proposal at T1 tier — not the hardcoded admin used\n// by Enable. Pause is consequential enough to warrant a tier-restricted\n// governance vote rather than a single-multisig click. T1 filter\n// prevents lower-tier members from spamming pause proposals to dilute\n// attention. Trade-off is response time: a T1 vote takes hours-to-days;\n// if a faster emergency-halt mechanism is needed, that belongs at a\n// different layer (e.g. an ante-handler-level chain pause), not here.\n//\n// Pause is orthogonal to the pre-Enable bypass: before Enable, the\n// verifier returns true regardless of paused state. So executing a\n// pause proposal before Enable has no effect on deploys, but the\n// value persists and applies the moment Enable runs. To avoid this\n// staging trap, operators should call Enable BEFORE any pause\n// proposals are voted in.\n//\n// Idempotency: calling ProposeSetPaused(v) when the realm's current\n// paused state already equals v panics at proposal-creation time so\n// voters never see a proposal whose execution would no-op.\nfunc ProposeSetPaused(cur realm, v bool) dao.ProposalRequest {\n\tif paused == v {\n\t\tpanic(\"paused state already matches requested value; no-op proposal rejected\")\n\t}\n\tcb := func(cur realm) error {\n\t\tsetPaused(0, cur, v)\n\t\treturn nil\n\t}\n\ttitle := \"Unpause Namespace Verifier\"\n\tdesc := \"This proposal unpauses `r/sys/names`. After execution, the namespace verifier will resume normal authorization checks (PA + registered-name paths). MsgCall traffic to existing realms is unaffected (pause was scoped to MsgAddPackage); only NEW package deploys gate on the unpaused state.\"\n\tif v {\n\t\ttitle = \"Pause Namespace Verifier\"\n\t\tdesc = \"This proposal pauses `r/sys/names`. After execution, the namespace verifier will reject EVERY new MsgAddPackage on the chain — PA (personal-address) and registered-name namespaces alike — until a subsequent unpause proposal executes. This is an emergency halt scoped to addpkg; MsgCall traffic to existing realms is unaffected.\"\n\t}\n\treturn dao.NewProposalRequestWithFilter(\n\t\ttitle,\n\t\tdesc,\n\t\tdao.NewSimpleExecutor(0, cur, cb, \"\"),\n\t\tgovimpl.NewFilterByTier(memberstore.T1),\n\t)\n}\n\n// setPaused is the private actuator behind ProposeSetPaused's executor\n// callback. It is unexported to ensure no path outside the proposal\n// flow can flip the flag — every state change goes through GovDAO.\n//\n// Emits NamespaceEnforcement{Paused,Unpaused} with the executor's\n// realm path for off-chain audit trails.\nfunc setPaused(_ int, rlm realm, v bool) {\n\tpaused = v\n\tif v {\n\t\tchain.Emit(\"NamespaceEnforcementPaused\", \"by\", rlm.Previous().PkgPath())\n\t} else {\n\t\tchain.Emit(\"NamespaceEnforcementUnpaused\", \"by\", rlm.Previous().PkgPath())\n\t}\n}\n\n// IsPaused reports the current value of the pause flag. Note: when\n// the realm is pre-Enable, IsPaused may return true but the verifier\n// will still pass-through (pre-Enable bypass takes priority).\nfunc IsPaused() bool {\n\treturn paused\n}\n\n// verifier checks namespace deployment permissions.\n// lookup is the registered-name resolver — pass nil to disable that path\n// (used by tests that want to exercise PA-only behavior).\n//\n// Order of checks (top to bottom, first matching wins):\n//  1. !isEnabled → return true   (pre-Enable: testing/dev bypass)\n//  2. isPaused   → return false  (emergency halt — INCLUDES PA)\n//  3. invalid input → return false\n//  4. PA match (addr.String() == namespace) → return true\n//  5. Registered-name lookup match → return true\n//  6. otherwise → return false\n//\n// The pause check is intentionally above the PA check. A SetPaused(true)\n// halts every deploy regardless of namespace shape.\nfunc verifier(isEnabled, isPaused bool, address_XXX address, namespace string, lookup nameLookupFn) bool {\n\tif !isEnabled {\n\t\treturn true // pre-genesis / dev convenience: bypass everything\n\t}\n\n\tif isPaused {\n\t\treturn false // emergency halt: reject every deploy including PA\n\t}\n\n\tif namespace == \"\" || !address_XXX.IsValid() {\n\t\treturn false\n\t}\n\n\t// Path 1: PA (personal-address) namespace.\n\t// gno.land/{p,r}/{ADDRESS}/**\n\tif address_XXX.String() == namespace {\n\t\treturn true\n\t}\n\n\t// Path 2: registered-name namespace via r/sys/users.\n\tif lookup != nil {\n\t\tif owner, ok := lookup(namespace); ok \u0026\u0026 owner == address_XXX {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"int256","path":"gno.land/p/onbloc/int256","files":[{"name":"arithmetic.gno","body":"package int256\n\nimport (\n\t\"gno.land/p/onbloc/uint256\"\n)\n\nconst divisionByZeroError = \"division by zero\"\n\n// Add adds two int256 values and saves the result in z.\nfunc (z *Int) Add(x, y *Int) *Int {\n\tz.value.Add(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// AddUint256 adds int256 and uint256 values and saves the result in z.\nfunc (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int {\n\tz.value.Add(\u0026x.value, y)\n\treturn z\n}\n\n// Sub subtracts two int256 values and saves the result in z.\nfunc (z *Int) Sub(x, y *Int) *Int {\n\tz.value.Sub(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// SubUint256 subtracts uint256 and int256 values and saves the result in z.\nfunc (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int {\n\tz.value.Sub(\u0026x.value, y)\n\treturn z\n}\n\n// Mul multiplies two int256 values and saves the result in z.\n//\n// It considers the signs of the operands to determine the sign of the result.\nfunc (z *Int) Mul(x, y *Int) *Int {\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs, ySign := y.Abs(), y.Sign()\n\n\tz.value.Mul(xAbs, yAbs)\n\n\tif xSign != ySign {\n\t\tz.value.Neg(\u0026z.value)\n\t}\n\n\treturn z\n}\n\n// Abs returns the absolute value of z.\nfunc (z *Int) Abs() *uint256.Uint {\n\tif z.Sign() \u003e= 0 {\n\t\treturn \u0026z.value\n\t}\n\n\tvar absValue uint256.Uint\n\tabsValue.Sub(uint0, \u0026z.value).Neg(\u0026z.value)\n\n\treturn \u0026absValue\n}\n\n// Div performs integer division z = x / y and returns z.\n// If y == 0, it panics with a \"division by zero\" error.\n//\n// This function handles signed division using two's complement representation:\n//  1. Determine the sign of the quotient based on the signs of x and y.\n//  2. Perform unsigned division on the absolute values.\n//  3. Adjust the result's sign if necessary.\n//\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Let x = -6 (11111010 in two's complement) and y = 3 (00000011)\n//\n// Step 2: Determine signs\n//\n//\tx: negative (MSB is 1)\n//\ty: positive (MSB is 0)\n//\n// Step 3: Calculate absolute values\n//\n//\t|x| = 6:  11111010 -\u003e 00000110\n//\t     NOT: 00000101\n//\t     +1:  00000110\n//\n//\t|y| = 3:  00000011 (already positive)\n//\n// Step 4: Unsigned division\n//\n//\t6 / 3 = 2:  00000010\n//\n// Step 5: Adjust sign (x and y have different signs)\n//\n//\t-2:  00000010 -\u003e 11111110\n//\t     NOT: 11111101\n//\t     +1:  11111110\n//\n// Note: This implementation rounds towards zero, as is standard in Go.\nfunc (z *Int) Div(x, y *Int) *Int {\n\t// Step 1: Check for division by zero\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Step 2, 3: Calculate the absolute values of x and y\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs, ySign := y.Abs(), y.Sign()\n\n\t// Step 4: Perform unsigned division on the absolute values\n\tz.value.Div(xAbs, yAbs)\n\n\t// Step 5: Adjust the sign of the result\n\t// if x and y have different signs, the result must be negative\n\tif xSign != ySign {\n\t\tz.value.Neg(\u0026z.value)\n\t}\n\n\treturn z\n}\n\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Let x = -7 (11111001 in two's complement) and y = 3 (00000011)\n//\n// Step 2: Determine signs\n//\n//\tx: negative (MSB is 1)\n//\ty: positive (MSB is 0)\n//\n// Step 3: Calculate absolute values\n//\n//\t|x| = 7:  11111001 -\u003e 00000111\n//\t     NOT: 00000110\n//\t     +1:  00000111\n//\n//\t|y| = 3:  00000011 (already positive)\n//\n// Step 4: Unsigned division\n//\n//\t7 / 3 = 2:  00000010\n//\n// Step 5: Adjust sign (x and y have different signs)\n//\n//\t-2:  00000010 -\u003e 11111110\n//\t     NOT: 11111101\n//\t     +1:  11111110\n//\n// Final result: -2 (11111110 in two's complement)\n//\n// Note: This implementation rounds towards zero, as is standard in Go.\nfunc (z *Int) Quo(x, y *Int) *Int {\n\t// Step 1: Check for division by zero\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Step 2, 3: Calculate the absolute values of x and y\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs, ySign := y.Abs(), y.Sign()\n\n\t// perform unsigned division on the absolute values\n\tz.value.Div(xAbs, yAbs)\n\n\t// Step 5: Adjust the sign of the result\n\t// if x and y have different signs, the result must be negative\n\tif xSign != ySign {\n\t\tz.value.Neg(\u0026z.value)\n\t}\n\n\treturn z\n}\n\n// Rem sets z to the remainder x%y for y != 0 and returns z.\n//\n// The function performs the following steps:\n//  1. Check for division by zero\n//  2. Determine the signs of x and y\n//  3. Calculate the absolute values of x and y\n//  4. Perform unsigned division and get the remainder\n//  5. Adjust the sign of the remainder\n//\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Let x = -7 (11111001 in two's complement) and y = 3 (00000011)\n//\n// Step 2: Determine signs\n//\n//\tx: negative (MSB is 1)\n//\ty: positive (MSB is 0)\n//\n// Step 3: Calculate absolute values\n//\n//\t|x| = 7:  11111001 -\u003e 00000111\n//\t     NOT: 00000110\n//\t     +1:  00000111\n//\n//\t|y| = 3:  00000011 (already positive)\n//\n// Step 4: Unsigned division\n//\n//\t7 / 3 = 2 remainder 1\n//\tq = 2:  00000010 (not used in result)\n//\tr = 1:  00000001\n//\n// Step 5: Adjust sign of remainder (x is negative)\n//\n//\t-1:  00000001 -\u003e 11111111\n//\t     NOT: 11111110\n//\t     +1:  11111111\n//\n// Final result: -1 (11111111 in two's complement)\n//\n// Note: The sign of the remainder is always the same as the sign of the dividend (x).\nfunc (z *Int) Rem(x, y *Int) *Int {\n\t// Step 1: Check for division by zero\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Step 2, 3\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs := y.Abs()\n\n\t// Step 4: Perform unsigned division and get the remainder\n\tvar q, r uint256.Uint\n\tq.DivMod(xAbs, yAbs, \u0026r)\n\n\t// Step 5: Adjust the sign of the remainder\n\tif xSign \u003c 0 {\n\t\tr.Neg(\u0026r)\n\t}\n\n\tz.value.Set(\u0026r)\n\treturn z\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// The result (z) has the same sign as the divisor y.\nfunc (z *Int) Mod(x, y *Int) *Int {\n\treturn z.ModE(x, y)\n}\n\n// DivE performs Euclidean division of x by y, setting z to the quotient and returning z.\n// If y == 0, it panics with a \"division by zero\" error.\n//\n// Euclidean division satisfies the following properties:\n//  1. The remainder is always non-negative: 0 \u003c= x mod y \u003c |y|\n//  2. It follows the identity: x = y * (x div y) + (x mod y)\nfunc (z *Int) DivE(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Compute the truncated division quotient\n\tz.Quo(x, y)\n\n\t// Compute the remainder\n\tr := new(Int).Rem(x, y)\n\n\t// If the remainder is negative, adjust the quotient\n\tif r.Sign() \u003c 0 {\n\t\tif y.Sign() \u003e 0 {\n\t\t\tz.Sub(z, NewInt(1))\n\t\t} else {\n\t\t\tz.Add(z, NewInt(1))\n\t\t}\n\t}\n\n\treturn z\n}\n\n// ModE computes the Euclidean modulus of x by y, setting z to the result and returning z.\n// If y == 0, it panics with a \"division by zero\" error.\n//\n// The Euclidean modulus is always non-negative and satisfies:\n//\n//\t0 \u003c= x mod y \u003c |y|\n//\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Case 1: Let x = -7 (11111001 in two's complement) and y = 3 (00000011)\n//\n// Step 1: Compute remainder (using Rem)\n//\n//\tResult of Rem: -1 (11111111 in two's complement)\n//\n// Step 2: Adjust sign (result is negative, y is positive)\n//\n//\t-1 + 3 = 2\n//\t11111111 + 00000011 = 00000010\n//\n// Final result: 2 (00000010)\n//\n// Case 2: Let x = -7 (11111001 in two's complement) and y = -3 (11111101 in two's complement)\n//\n// Step 1: Compute remainder (using Rem)\n//\n//\tResult of Rem: -1 (11111111 in two's complement)\n//\n// Step 2: Adjust sign (result is negative, y is negative)\n//\n//\tNo adjustment needed\n//\n// Final result: -1 (11111111 in two's complement)\n//\n// Note: This implementation ensures that the result always has the same sign as y,\n// which is different from the Rem operation.\nfunc (z *Int) ModE(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Perform T-division to get the remainder\n\tz.Rem(x, y)\n\n\t// Adjust the remainder if necessary\n\tif z.Sign() \u003e= 0 {\n\t\treturn z\n\t}\n\tif y.Sign() \u003e 0 {\n\t\treturn z.Add(z, y)\n\t}\n\n\treturn z.Sub(z, y)\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\n//\n// If the y is positive, it adds y.value to x. otherwise, it subtracts y.Abs() from x.\nfunc AddDelta(z, x *uint256.Uint, y *Int) {\n\tif y.Sign() \u003e= 0 {\n\t\tz.Add(x, \u0026y.value)\n\t} else {\n\t\tz.Sub(x, y.Abs())\n\t}\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\n//\n// This function returns true if the addition overflows, false otherwise.\nfunc AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool {\n\tvar overflow bool\n\tif y.Sign() \u003e= 0 {\n\t\t_, overflow = z.AddOverflow(x, \u0026y.value)\n\t} else {\n\t\tvar absY uint256.Uint\n\t\tabsY.Sub(uint0, \u0026y.value) // absY = -y.value\n\t\t_, overflow = z.SubOverflow(x, \u0026absY)\n\t}\n\n\treturn overflow\n}\n"},{"name":"bitwise.gno","body":"package int256\n\n// Not sets z to the bitwise NOT of x and returns z.\n//\n// The bitwise NOT operation flips each bit of the operand.\nfunc (z *Int) Not(x *Int) *Int {\n\tz.value.Not(\u0026x.value)\n\treturn z\n}\n\n// And sets z to the bitwise AND of x and y and returns z.\n//\n// The bitwise AND operation results in a value that has a bit set\n// only if both corresponding bits of the operands are set.\nfunc (z *Int) And(x, y *Int) *Int {\n\tz.value.And(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// Or sets z to the bitwise OR of x and y and returns z.\n//\n// The bitwise OR operation results in a value that has a bit set\n// if at least one of the corresponding bits of the operands is set.\nfunc (z *Int) Or(x, y *Int) *Int {\n\tz.value.Or(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// Xor sets z to the bitwise XOR of x and y and returns z.\n//\n// The bitwise XOR operation results in a value that has a bit set\n// only if the corresponding bits of the operands are different.\nfunc (z *Int) Xor(x, y *Int) *Int {\n\tz.value.Xor(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// Rsh sets z to the result of right-shifting x by n bits and returns z.\n//\n// Right shift operation moves all bits in the operand to the right by the specified number of positions.\n// Bits shifted out on the right are discarded, and zeros are shifted in on the left.\nfunc (z *Int) Rsh(x *Int, n uint) *Int {\n\tz.value.Rsh(\u0026x.value, n)\n\treturn z\n}\n\n// Lsh sets z to the result of left-shifting x by n bits and returns z.\n//\n// Left shift operation moves all bits in the operand to the left by the specified number of positions.\n// Bits shifted out on the left are discarded, and zeros are shifted in on the right.\nfunc (z *Int) Lsh(x *Int, n uint) *Int {\n\tz.value.Lsh(\u0026x.value, n)\n\treturn z\n}\n"},{"name":"cmp.gno","body":"package int256\n\nfunc (z *Int) Eq(x *Int) bool {\n\treturn z.value.Eq(\u0026x.value)\n}\n\nfunc (z *Int) Neq(x *Int) bool {\n\treturn !z.Eq(x)\n}\n\n// Cmp compares z and x and returns:\n//\n//   - 1 if z \u003e x\n//   - 0 if z == x\n//   - -1 if z \u003c x\nfunc (z *Int) Cmp(x *Int) int {\n\tzSign, xSign := z.Sign(), x.Sign()\n\n\tif zSign == xSign {\n\t\treturn z.value.Cmp(\u0026x.value)\n\t}\n\n\tif zSign == 0 {\n\t\treturn -xSign\n\t}\n\n\treturn zSign\n}\n\n// IsZero returns true if z == 0\nfunc (z *Int) IsZero() bool {\n\treturn z.value.IsZero()\n}\n\n// IsNeg returns true if z \u003c 0\nfunc (z *Int) IsNeg() bool {\n\treturn z.Sign() \u003c 0\n}\n\nfunc (z *Int) Lt(x *Int) bool {\n\treturn z.Cmp(x) \u003c 0\n}\n\nfunc (z *Int) Gt(x *Int) bool {\n\treturn z.Cmp(x) \u003e 0\n}\n\nfunc (z *Int) Le(x *Int) bool {\n\treturn z.Cmp(x) \u003c= 0\n}\n\nfunc (z *Int) Ge(x *Int) bool {\n\treturn z.Cmp(x) \u003e= 0\n}\n\n// Clone creates a new Int identical to z\nfunc (z *Int) Clone() *Int {\n\treturn New().FromUint256(\u0026z.value)\n}\n"},{"name":"conversion.gno","body":"package int256\n\nimport (\n\t\"math\"\n\n\t\"gno.land/p/onbloc/uint256\"\n)\n\n// SetInt64 sets the Int to the value of the provided int64.\n//\n// This method allows for easy conversion from standard Go integer types\n// to Int, correctly handling both positive and negative values.\nfunc (z *Int) SetInt64(v int64) *Int {\n\tif v \u003e= 0 {\n\t\tz.value.SetUint64(uint64(v))\n\t} else {\n\t\tz.value.SetUint64(uint64(-v)).Neg(\u0026z.value)\n\t}\n\treturn z\n}\n\n// SetUint64 sets the Int to the value of the provided uint64.\nfunc (z *Int) SetUint64(v uint64) *Int {\n\tz.value.SetUint64(v)\n\treturn z\n}\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Int) Uint64() uint64 {\n\tif z.Sign() \u003c 0 {\n\t\tpanic(\"cannot convert negative int256 to uint64\")\n\t}\n\tif z.value.Gt(uint256.NewUint(0).SetUint64(math.MaxUint64)) {\n\t\tpanic(\"overflow: int256 does not fit in uint64 type\")\n\t}\n\treturn z.value.Uint64()\n}\n\n// Int64 returns the lower 64-bits of z\nfunc (z *Int) Int64() int64 {\n\tif z.Sign() \u003e= 0 {\n\t\tif z.value.BitLen() \u003e 64 {\n\t\t\tpanic(\"overflow: int256 does not fit in int64 type\")\n\t\t}\n\t\treturn int64(z.value.Uint64())\n\t}\n\tvar temp uint256.Uint\n\ttemp.Sub(uint256.NewUint(0), \u0026z.value) // temp = -z.value\n\tif temp.BitLen() \u003e 64 {\n\t\tpanic(\"overflow: int256 does not fit in int64 type\")\n\t}\n\treturn -int64(temp.Uint64())\n}\n\n// Neg sets z to -x and returns z.)\nfunc (z *Int) Neg(x *Int) *Int {\n\tif x.IsZero() {\n\t\tz.value.Clear()\n\t} else {\n\t\tz.value.Neg(\u0026x.value)\n\t}\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Int) Set(x *Int) *Int {\n\tz.value.Set(\u0026x.value)\n\treturn z\n}\n\n// SetFromUint256 converts a uint256.Uint to Int and sets the value to z.\nfunc (z *Int) SetUint256(x *uint256.Uint) *Int {\n\tz.value.Set(x)\n\treturn z\n}\n\n// ToString returns a string representation of z in base 10.\n// The string is prefixed with a minus sign if z is negative.\nfunc (z *Int) String() string {\n\tif z.value.IsZero() {\n\t\treturn \"0\"\n\t}\n\tsign := z.Sign()\n\tvar temp uint256.Uint\n\tif sign \u003e= 0 {\n\t\ttemp.Set(\u0026z.value)\n\t} else {\n\t\t// temp = -z.value\n\t\ttemp.Sub(uint256.NewUint(0), \u0026z.value)\n\t}\n\ts := temp.Dec()\n\tif sign \u003c 0 {\n\t\treturn \"-\" + s\n\t}\n\treturn s\n}\n\n// NilToZero returns the Int if it's not nil, or a new zero-valued Int otherwise.\n//\n// This method is useful for safely handling potentially nil Int pointers,\n// ensuring that operations always have a valid Int to work with.\nfunc (z *Int) NilToZero() *Int {\n\tif z == nil {\n\t\treturn Zero()\n\t}\n\treturn z\n}\n"},{"name":"doc.gno","body":"// The int256 package provides a 256-bit signed interger type for gno,\n// supporting arithmetic operations and bitwise manipulation.\n//\n// It designed for applications that require high-precision arithmetic\n// beyond the standard 64-bit range.\n//\n// ## Features\n//\n//   - 256-bit Signed Integers: Support for large integer ranging from -2^255 to 2^255-1.\n//   - Two's Complement Representation: Efficient storage and computation using two's complement.\n//   - Arithmetic Operations: Add, Sub, Mul, Div, Mod, Inc, Dec, etc.\n//   - Bitwise Operations: And, Or, Xor, Not, etc.\n//   - Comparison Operations: Cmp, Eq, Lt, Gt, etc.\n//   - Conversion Functions: Int to Uint, Uint to Int, etc.\n//   - String Parsing and Formatting: Convert to and from decimal string representation.\n//\n// ## Notes\n//\n//   - Some methods may panic when encountering invalid inputs or overflows.\n//   - The `int256.Int` type can interact with `uint256.Uint` from the `p/demo/uint256` package.\n//   - Unlike `math/big.Int`, the `int256.Int` type has fixed size (256-bit) and does not support\n//     arbitrary precision arithmetic.\n//\n// # Division and modulus operations\n//\n// This package provides three different division and modulus operations:\n//\n//   - Div and Rem: Truncated division (T-division)\n//   - Quo and Mod: Floored division (F-division)\n//   - DivE and ModE: Euclidean division (E-division)\n//\n// Truncated division (Div, Rem) is the most common implementation in modern processors\n// and programming languages. It rounds quotients towards zero and the remainder\n// always has the same sign as the dividend.\n//\n// Floored division (Quo, Mod) always rounds quotients towards negative infinity.\n// This ensures that the modulus is always non-negative for a positive divisor,\n// which can be useful in certain algorithms.\n//\n// Euclidean division (DivE, ModE) ensures that the remainder is always non-negative,\n// regardless of the signs of the dividend and divisor. This has several mathematical\n// advantages:\n//\n//  1. It satisfies the unique division with remainder theorem.\n//  2. It preserves division and modulus properties for negative divisors.\n//  3. It allows for optimizations in divisions by powers of two.\n//\n// [+] Currently, ModE and Mod are shared the same implementation.\n//\n// ## Performance considerations:\n//\n//   - For most operations, the performance difference between these division types is negligible.\n//   - Euclidean division may require an extra comparison and potentially an addition,\n//     which could impact performance in extremely performance-critical scenarios.\n//   - For divisions by powers of two, Euclidean division can be optimized to use\n//     bitwise operations, potentially offering better performance.\n//\n// ## Usage guidelines:\n//\n//   - Use Div and Rem for general-purpose division that matches most common expectations.\n//   - Use Quo and Mod when you need a non-negative remainder for positive divisors,\n//     or when implementing algorithms that assume floored division.\n//   - Use DivE and ModE when you need the mathematical properties of Euclidean division,\n//     or when working with algorithms that specifically require it.\n//\n// Note: When working with negative numbers, be aware of the differences in behavior\n// between these division types, especially at the boundaries of integer ranges.\n//\n// ## References\n//\n// Daan Leijen, “Division and Modulus for Computer Scientists”:\n// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf\npackage int256\n"},{"name":"gnomod.toml","body":"module = \"gno.land/p/onbloc/int256\"\ngno = \"0.9\"\n"},{"name":"int256.gno","body":"package int256\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/onbloc/uint256\"\n)\n\nvar (\n\tint1  = NewInt(1)\n\tuint0 = uint256.NewUint(0)\n\tuint1 = uint256.NewUint(1)\n)\n\ntype Int struct {\n\tvalue uint256.Uint\n}\n\n// New creates and returns a new Int initialized to zero.\nfunc New() *Int {\n\treturn \u0026Int{}\n}\n\n// NewInt allocates and returns a new Int set to the value of the provided int64.\nfunc NewInt(x int64) *Int {\n\treturn New().SetInt64(x)\n}\n\n// Zero returns a new Int initialized to 0.\n//\n// This function is useful for creating a starting point for calculations or\n// when an explicit zero value is needed.\nfunc Zero() *Int { return \u0026Int{} }\n\n// One returns a new Int initialized to one.\n//\n// This function is convenient for operations that require a unit value,\n// such as incrementing or serving as an identity element in multiplication.\nfunc One() *Int {\n\treturn \u0026Int{\n\t\tvalue: *uint256.NewUint(1),\n\t}\n}\n\n// Sign determines the sign of the Int.\n//\n// It returns -1 for negative numbers, 0 for zero, and +1 for positive numbers.\nfunc (z *Int) Sign() int {\n\tif z == nil || z.IsZero() {\n\t\treturn 0\n\t}\n\t// Right shift the value by 255 bits to check the sign bit.\n\t// In two's complement representation, the most significant bit (MSB) is the sign bit.\n\t// If the MSB is 0, the number is positive; if it is 1, the number is negative.\n\t//\n\t// Example:\n\t// Original value:  1 0 1 0 ... 0 1  (256 bits)\n\t// After Rsh 255:   0 0 0 0 ... 0 1  (1 bit)\n\t//\n\t// This approach is highly efficient as it avoids the need for comparisons\n\t// or arithmetic operations on the full 256-bit number. Instead it reduces\n\t// the problem to checking a single bit.\n\t//\n\t// Additionally, this method will work correctly for all values,\n\t// including the minimum possible negative number (which in two's complement\n\t// doesn't have a positive counterpart in the same bit range).\n\tvar temp uint256.Uint\n\tif temp.Rsh(\u0026z.value, 255).IsZero() {\n\t\treturn 1\n\t}\n\treturn -1\n}\n\n// FromDecimal creates a new Int from a decimal string representation.\n// It handles both positive and negative values.\n//\n// This function is useful for parsing user input or reading numeric data\n// from text-based formats.\nfunc FromDecimal(s string) (*Int, error) {\n\treturn New().SetString(s)\n}\n\n// MustFromDecimal is similar to FromDecimal but panics if the input string\n// is not a valid decimal representation.\nfunc MustFromDecimal(s string) *Int {\n\tz, err := FromDecimal(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn z\n}\n\n// SetString sets the Int to the value represented by the input string.\n// This method supports decimal string representations of integers and handles\n// both positive and negative values.\nfunc (z *Int) SetString(s string) (*Int, error) {\n\tif len(s) == 0 {\n\t\treturn nil, errors.New(\"cannot set int256 from empty string\")\n\t}\n\n\t// Check for negative sign\n\tneg := s[0] == '-'\n\tif neg || s[0] == '+' {\n\t\ts = s[1:]\n\t}\n\n\t// Convert string to uint256\n\ttemp, err := uint256.FromDecimal(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If negative, negate the uint256 value\n\tif neg {\n\t\ttemp.Neg(temp)\n\t}\n\n\tz.value.Set(temp)\n\treturn z, nil\n}\n\n// FromUint256 sets the Int to the value of the provided Uint256.\n//\n// This method allows for conversion from unsigned 256-bit integers\n// to signed integers.\nfunc (z *Int) FromUint256(v *uint256.Uint) *Int {\n\tz.value.Set(v)\n\treturn z\n}\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"rewards","path":"gno.land/r/sys/rewards","files":[{"name":"gnomod.toml","body":"module = \"gno.land/r/sys/rewards\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"},{"name":"rewards.gno","body":"// This package will be used to manage proof-of-contributions on the exposed smart-contract side.\npackage rewards\n\n// TODO: write specs.\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_addpkg","creator":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","package":{"name":"draftrealm","path":"gno.land/r/demo/draftrealm","files":[{"name":"draftrealm.gno","body":"package draftrealm\n\n// this realm is an example of a draft realm\n// it can only be deployed and imported by packages added at genesis time\n\nfunc Render(path string) string {\n\treturn \"draft\"\n}\n"},{"name":"gnomod.toml","body":"module = \"gno.land/r/demo/draftrealm\"\ngno = \"0.9\"\ndraft = true\n"}],"type":{"@type":"/gno.MemPackageType","value":"MPUserProd"}},"send":"","max_deposit":""}],"fee":{"gas_wanted":"50000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}},{"tx":{"msg":[{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","max_deposit":"","pkg_path":"gno.land/r/sys/users/init","func":"Bootstrap"}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":null,"signature":null}],"memo":""},"metadata":{"timestamp":"1786351772"}}],"auth":{"params":{"max_memo_bytes":"65536","tx_sig_limit":"7","tx_size_cost_per_byte":"10","sig_verify_cost_ed25519":"590","sig_verify_cost_secp256k1":"1000","gas_price_change_compressor":"10","target_gas_ratio":"70","initial_gasprice":{"gas":"1000","price":"1ugnot"},"unrestricted_addrs":null,"fee_collector":"g17xpfvakm2amg962yls6f84z3kell8c5lr9lr2e"}},"bank":{"params":{"restricted_denoms":[]}},"vm":{"params":{"sysnames_pkgpath":"gno.land/r/sys/names","syscla_pkgpath":"gno.land/r/sys/cla","chain_domain":"gno.land","default_deposit":"600000000ugnot","storage_price":"100ugnot","storage_fee_collector":"g1c9stkafpvcwez2efq3qtfuezw4zpaux3tvxggk","min_get_read_depth_100":"100","min_set_read_depth_100":"200","min_write_depth_100":"540","fixed_get_read_depth_100":"100","fixed_set_read_depth_100":"200","fixed_write_depth_100":"540","iter_next_cost_flat":"1000","preprocess_gas_per_byte":"1250"},"realm_params":null}}}}}